PluginProbe
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites / 2.6.4
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites v2.6.4
4.1.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 2.1.2 2.5.0 2.5.3 2.6.1 All 38 releases
blockspare / freemius / includes / class-freemius.php

class-freemius.php in BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites 2.6.4, at freemius/includes/class-freemius.php

26,405 lines 965.3 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 1.1.7.5
114 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
115 */
116 private $_anonymous_mode;
117
118 /**
119 * @since 1.1.9
120 * @var bool Hints the SDK if plugin have any free plans.
121 */
122 private $_is_premium_only;
123
124 /**
125 * @since 1.2.1.6
126 * @var bool Hints the SDK if plugin have premium code version at all.
127 */
128 private $_has_premium_version;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
133 */
134 private $_ignore_pending_mode;
135
136 /**
137 * @since 1.0.8
138 * @var bool Hints the SDK if the plugin has any paid plans.
139 */
140 private $_has_paid_plans;
141
142 /**
143 * @since 1.2.1.5
144 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
145 * without a specified period, if positive - the number of trial days.
146 */
147 private $_trial_days = - 1;
148
149 /**
150 * @since 1.2.1.5
151 * @var bool Hints the SDK if the trial requires a payment method or not.
152 */
153 private $_is_trial_require_payment = false;
154
155 /**
156 * @since 1.0.7
157 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
158 */
159 private $_is_org_compliant;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is has add-ons.
164 */
165 private $_has_addons;
166
167 /**
168 * @since 2.4.5
169 * @var string Navigation type: 'menu' or 'tabs'.
170 */
171 private $_navigation;
172
173 const NAVIGATION_MENU = 'menu';
174 const NAVIGATION_TABS = 'tabs';
175
176 /**
177 * @since 1.1.6
178 * @var string[]bool.
179 */
180 private $_permissions;
181
182 /**
183 * @var FS_Storage
184 */
185 private $_storage;
186
187 /**
188 * @since 1.2.2.7
189 * @var FS_Cache_Manager
190 */
191 private $_cache;
192
193 /**
194 * @since 1.0.0
195 *
196 * @var FS_Logger
197 */
198 private $_logger;
199 /**
200 * @since 1.0.4
201 *
202 * @var FS_Plugin
203 */
204 private $_plugin = false;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin|false
209 */
210 private $_parent_plugin = false;
211 /**
212 * @since 1.1.1
213 *
214 * @var Freemius
215 */
216 private $_parent = false;
217 /**
218 * @since 1.0.1
219 *
220 * @var FS_User
221 */
222 private $_user = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_Site
227 */
228 private $_site = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Plugin_License
233 */
234 private $_license;
235 /**
236 * @since 1.0.2
237 *
238 * @var FS_Plugin_Plan[]
239 */
240 private $_plans = false;
241 /**
242 * @var FS_Plugin_License[]
243 * @since 1.0.5
244 */
245 private $_licenses = false;
246
247 /**
248 * @since 1.0.1
249 *
250 * @var FS_Admin_Menu_Manager
251 */
252 private $_menu;
253
254 /**
255 * @var FS_Admin_Notices
256 */
257 private $_admin_notices;
258
259 /**
260 * @since 1.1.6
261 *
262 * @var FS_Admin_Notices
263 */
264 private static $_global_admin_notices;
265
266 /**
267 * @var FS_Logger
268 * @since 1.0.0
269 */
270 private static $_static_logger;
271
272 /**
273 * @var FS_Options
274 * @since 1.0.2
275 */
276 private static $_accounts;
277
278 /**
279 * @since 1.2.2
280 *
281 * @var number
282 */
283 private $_module_id;
284
285 /**
286 * @var Freemius[]
287 */
288 private static $_instances = array();
289
290 /**
291 * @since 1.2.3
292 *
293 * @var FS_Affiliate
294 */
295 private $affiliate = null;
296
297 /**
298 * @since 1.2.3
299 *
300 * @var FS_AffiliateTerms
301 */
302 private $plugin_affiliate_terms = null;
303
304 /**
305 * @since 1.2.3
306 *
307 * @var FS_AffiliateTerms
308 */
309 private $custom_affiliate_terms = null;
310
311 /**
312 * @since 2.0.0
313 *
314 * @var bool
315 */
316 private $_is_multisite_integrated;
317
318 /**
319 * @since 2.0.0
320 *
321 * @var bool True if the current request is for a network admin screen and the plugin is network active.
322 */
323 private $_is_network_active;
324
325 /**
326 * @since 2.0.0
327 *
328 * @var int|null The original blog ID the plugin was loaded with.
329 */
330 private $_blog_id = null;
331
332 /**
333 * @since 2.0.0
334 *
335 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
336 */
337 private $_context_is_network_or_blog_id = null;
338
339 /**
340 * @since 2.0.0
341 *
342 * @var string
343 */
344 private $_dynamically_added_top_level_page_hook_name = '';
345
346 /**
347 * @author Leo Fajardo (@leorw)
348 * @since 2.3.1
349 *
350 * @var bool
351 */
352 private $is_whitelabeled;
353
354 /**
355 * @author Leo Fajardo (@leorw)
356 * @since 2.4.0
357 *
358 * @var bool
359 */
360 private $_is_bundle_license_auto_activation_enabled = false;
361
362 #region Uninstall Reasons IDs
363
364 const REASON_NO_LONGER_NEEDED = 1;
365 const REASON_FOUND_A_BETTER_PLUGIN = 2;
366 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
367 const REASON_BROKE_MY_SITE = 4;
368 const REASON_SUDDENLY_STOPPED_WORKING = 5;
369 const REASON_CANT_PAY_ANYMORE = 6;
370 const REASON_OTHER = 7;
371 const REASON_DIDNT_WORK = 8;
372 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
373 const REASON_COULDNT_MAKE_IT_WORK = 10;
374 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
375 const REASON_NOT_WORKING = 12;
376 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
377 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
378 const REASON_TEMPORARY_DEACTIVATION = 15;
379
380 #endregion
381
382 /**
383 * @author Leo Fajardo (@leorw)
384 * @since 2.3.1
385 *
386 * @var boolean|null
387 */
388 private $_use_external_pricing = null;
389 /**
390 * @author Leo Fajardo (@leorw)
391 * @since 2.4.2
392 *
393 * @var string|null
394 */
395 private $_pricing_js_path = null;
396
397 const VERSION_MAX_CHARS = 16;
398 const LANGUAGE_MAX_CHARS = 8;
399
400 /* Ctor
401 ------------------------------------------------------------------------------------------------------------------*/
402
403 /**
404 * Main singleton instance.
405 *
406 * @author Vova Feldman (@svovaf)
407 * @since 1.0.0
408 *
409 * @param number $module_id
410 * @param string|bool $slug
411 * @param bool $is_init Since 1.2.1 Is initiation sequence.
412 */
413 private function __construct( $module_id, $slug = false, $is_init = false ) {
414 $main_file = false;
415
416 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
417 $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
418 }
419
420 $this->_module_id = $module_id;
421 $this->_slug = $this->get_slug();
422 $this->_module_type = $this->get_module_type();
423
424 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
425
426 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
427
428 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
429
430 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
431
432 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
433 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
434 $this->_plugin_basename = $this->get_plugin_basename();
435 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
436
437 $this->_is_multisite_integrated = (
438 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
439 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
440 );
441
442 $this->_is_network_active = (
443 is_multisite() &&
444 $this->_is_multisite_integrated &&
445 // Themes are always network activated, but the ACTUAL activation is per site.
446 $this->is_plugin() &&
447 (
448 is_plugin_active_for_network( $this->_plugin_basename ) ||
449 // Plugin network level activation or uninstall.
450 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
451 )
452 );
453
454 $this->_storage->set_network_active(
455 $this->_is_network_active,
456 $this->is_delegated_connection()
457 );
458
459 if ( ! isset( $this->_storage->is_network_activated ) ) {
460 $this->_storage->is_network_activated = $this->_is_network_active;
461 }
462
463 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
464 // Update last activation level.
465 $this->_storage->is_network_activated = $this->_is_network_active;
466
467 $this->maybe_adjust_storage();
468 }
469
470 #region Migration
471
472 if ( is_multisite() ) {
473 /**
474 * If the install_timestamp exists on the site level but doesn't exist on the
475 * network level storage, it means that we need to process the storage with migration.
476 *
477 * 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.
478 *
479 * @author Vova Feldman (@svovaf)
480 * @since 2.0.0
481 */
482 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
483 false !== $this->_storage->get( 'install_timestamp', false, false )
484 ) {
485 // Initiate storage migration.
486 $this->_storage->migrate_to_network();
487
488 // Migrate module cache to network level storage.
489 $this->_cache->migrate_to_network();
490 }
491 }
492
493 #endregion
494
495 $base_name_split = explode( '/', $this->_plugin_basename );
496 $this->_plugin_dir_name = $base_name_split[0];
497
498 if ( $this->_logger->is_on() ) {
499 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
500 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
501 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
502 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
503 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
504 }
505
506 // Remember link between file to slug.
507 $this->store_file_slug_map();
508
509 // Store plugin's initial install timestamp.
510 if ( ! isset( $this->_storage->install_timestamp ) ) {
511 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
512 }
513
514 if ( ! is_object( $this->_plugin ) ) {
515 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
516 }
517
518 $this->_admin_notices = FS_Admin_Notices::instance(
519 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
520 /**
521 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
522 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
523 *
524 * @author Leo Fajardo (@leorw)
525 * @since 1.2.2
526 */
527 ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
528 $this->_plugin->title :
529 $this->get_plugin_name()
530 ),
531 $this->get_unique_affix()
532 );
533
534 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
535 fs_request_is_action( 'restart_freemius' )
536 ) {
537 FS_Api::clear_cache();
538 $this->_cache->clear();
539 }
540
541 $this->register_constructor_hooks();
542
543 /**
544 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
545 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
546 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
547 *
548 * @author Leo Fajardo (@leorw)
549 */
550 self::migrate_install_plan_to_plan_id( $this->_storage );
551
552 $this->_load_account();
553
554 $this->_version_updates_handler();
555 }
556
557 /**
558 * @author Leo Fajardo (@leorw)
559 * @since 2.3.0
560 */
561 private function maybe_adjust_storage() {
562 $install_timestamp = null;
563 $prev_is_premium = null;
564
565 $options_to_update = array();
566
567 $is_network_admin = fs_is_network_admin();
568
569 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
570
571 if ( ! $is_network_admin ) {
572 if ( is_null( $network_install_timestamp ) ) {
573 // Plugin was not network-activated before.
574 return;
575 }
576
577 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
578 // Set the `install_timestamp` only if it's not yet set.
579 $install_timestamp = $network_install_timestamp;
580 }
581
582 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
583 } else {
584 $current_wp_user = self::_get_current_wp_user();
585 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
586 $network_user_info = array();
587
588 $skips_count = 0;
589
590 $sites = self::get_sites();
591 $sites_count = count( $sites );
592
593 $blog_id_2_install_map = array();
594
595 $is_first_non_ignored_blog = true;
596
597 foreach ( $sites as $site ) {
598 $blog_id = self::get_site_blog_id( $site );
599
600 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
601
602 if ( is_null( $blog_install_timestamp ) ) {
603 // Plugin has not been installed on this blog.
604 continue;
605 }
606
607 $is_earlier_install = (
608 ! is_null( $install_timestamp ) &&
609 $blog_install_timestamp < $install_timestamp
610 );
611
612 $install = $this->get_install_by_blog_id( $blog_id );
613
614 $update_network_user_info = false;
615
616 if ( ! is_object( $install ) ) {
617 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
618 // The opt-in decision (whether to skip or opt in) is yet to be made.
619 continue;
620 }
621
622 $skips_count ++;
623 } else {
624 $blog_id_2_install_map[ $blog_id ] = $install;
625
626 if ( empty( $network_user_info ) ) {
627 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
628 $update_network_user_info = true;
629 }
630
631 if ( ! $update_network_user_info &&
632 is_object( $current_fs_user ) &&
633 $network_user_info['user_id'] != $current_fs_user->id &&
634 $install->user_id == $current_fs_user->id
635 ) {
636 // If an install that is owned by the current WP user is found, use its user information instead.
637 $update_network_user_info = true;
638 }
639
640 if ( ! $update_network_user_info &&
641 $is_earlier_install &&
642 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
643 ) {
644 // 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.
645 $update_network_user_info = true;
646 }
647 }
648
649 if ( $update_network_user_info ) {
650 $network_user_info = array(
651 'user_id' => $install->user_id,
652 'blog_id' => $blog_id
653 );
654 }
655
656 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
657
658 if ( $is_first_non_ignored_blog ) {
659 $prev_is_premium = $site_prev_is_premium;
660
661 if ( is_null( $network_install_timestamp ) ) {
662 $install_timestamp = $blog_install_timestamp;
663 }
664
665 $is_first_non_ignored_blog = false;
666
667 continue;
668 }
669
670 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
671 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
672 $prev_is_premium = null;
673 }
674
675 if ( $is_earlier_install ) {
676 // If an earlier install timestamp is found.
677 $install_timestamp = $blog_install_timestamp;
678 }
679 }
680
681 $installs_count = count( $blog_id_2_install_map );
682
683 if ( $sites_count === ( $installs_count + $skips_count ) ) {
684 if ( ! empty( $network_user_info ) ) {
685 $options_to_update['network_user_id'] = $network_user_info['user_id'];
686 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
687
688 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
689 if ( $install->user_id == $network_user_info['user_id'] ) {
690 continue;
691 }
692
693 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
694 }
695 }
696
697 if ( $sites_count === $skips_count ) {
698 /**
699 * Assume network-level skipping as the intended action if all actions identified were only
700 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
701 */
702 $options_to_update['is_anonymous_ms'] = true;
703 } else if ( $sites_count === $installs_count ) {
704 /**
705 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
706 * (i.e., no delegation and skipping of the connections so far).
707 */
708 $options_to_update['is_network_connected'] = true;
709 }
710 }
711 }
712
713 if ( ! is_null( $install_timestamp ) ) {
714 $options_to_update['install_timestamp'] = $install_timestamp;
715 }
716
717 if ( ! is_null( $prev_is_premium ) ) {
718 $options_to_update['prev_is_premium'] = $prev_is_premium;
719 }
720
721 if ( ! empty( $options_to_update ) ) {
722 $this->adjust_storage( $options_to_update, $is_network_admin );
723 }
724 }
725
726 /**
727 * @author Leo Fajardo (@leorw)
728 * @since 2.3.0
729 *
730 * @param array $options
731 * @param bool $is_network_admin
732 */
733 private function adjust_storage( $options, $is_network_admin ) {
734 foreach ( $options as $name => $value ) {
735 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
736 }
737 }
738
739 /**
740 * Checks whether this module has a settings menu.
741 *
742 * @author Leo Fajardo (@leorw)
743 * @since 1.2.2
744 *
745 * @return bool
746 */
747 function has_settings_menu() {
748 return ( $this->_is_network_active && fs_is_network_admin() ) ?
749 $this->_menu->has_network_menu() :
750 $this->_menu->has_menu();
751 }
752
753 /**
754 * 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.
755 *
756 * @author Vova Feldman (@svovaf)
757 * @since 2.4.5
758 *
759 * @return bool
760 */
761 function show_opt_in_on_themes_page() {
762 if ( ! $this->is_free_wp_org_theme() ) {
763 return false;
764 }
765
766 if ( ! $this->has_settings_menu() ) {
767 return true;
768 }
769
770 return $this->show_settings_with_tabs();
771 }
772
773 /**
774 * If `true` the opt-in should be shown on the product's main setting page.
775 *
776 * @author Vova Feldman (@svovaf)
777 * @since 2.4.5
778 *
779 * @return bool
780 *
781 * @uses show_opt_in_on_themes_page();
782 */
783 function show_opt_in_on_setting_page() {
784 return ! $this->show_opt_in_on_themes_page();
785 }
786
787 /**
788 * If `true` the settings should be shown using tabs.
789 *
790 * @author Vova Feldman (@svovaf)
791 * @since 2.4.5
792 *
793 * @return bool
794 */
795 function show_settings_with_tabs() {
796 return ( self::NAVIGATION_TABS === $this->_navigation );
797 }
798
799 /**
800 * Check if the context module is free wp.org theme.
801 *
802 * This method is helpful because:
803 * 1. wp.org themes are limited to a single submenu item,
804 * and sub-submenu items are most likely not allowed (never verified).
805 * 2. wp.org themes are not allowed to redirect the user
806 * after the theme activation, therefore, the agreed UX
807 * is showing the opt-in as a modal dialog box after
808 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
809 *
810 * @author Vova Feldman (@svovaf)
811 * @since 1.2.2.7
812 *
813 * @return bool
814 */
815 function is_free_wp_org_theme() {
816 return (
817 $this->is_theme() &&
818 $this->is_org_repo_compliant() &&
819 ! $this->is_premium()
820 );
821 }
822
823 /**
824 * Checks whether this a submenu item is visible.
825 *
826 * @author Vova Feldman (@svovaf)
827 * @since 1.2.2.6
828 * @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.
829 *
830 * @param string $slug
831 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
832 *
833 * @return bool
834 */
835 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
836 if ( $this->is_admin_page( $slug ) ) {
837 /**
838 * It is the current context page, so show the submenu item
839 * so the user will have the right context page, even if it
840 * was set to hidden.
841 */
842 return true;
843 }
844
845 if ( ! $this->has_settings_menu() ) {
846 // No menu settings at all.
847 return false;
848 }
849
850 if (
851 ! $is_tabs_visibility_check &&
852 $this->is_org_repo_compliant() &&
853 $this->show_settings_with_tabs()
854 ) {
855 /**
856 * wp.org themes are limited to a single submenu item, and
857 * sub-submenu items are most likely not allowed (never verified).
858 */
859 return false;
860 }
861
862 return $this->_menu->is_submenu_item_visible( $slug );
863 }
864
865 /**
866 * Check if a Freemius page should be accessible via the UI.
867 *
868 * @author Vova Feldman (@svovaf)
869 * @since 1.2.2.7
870 *
871 * @param string $slug
872 *
873 * @return bool
874 */
875 function is_page_visible( $slug ) {
876 if ( $this->is_admin_page( $slug ) ) {
877 return true;
878 }
879
880 return $this->_menu->is_submenu_item_visible( $slug, true, true );
881 }
882
883 /**
884 * @author Vova Feldman (@svovaf)
885 * @since 1.0.9
886 */
887 private function _version_updates_handler() {
888 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
889 // Freemius version upgrade mode.
890 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
891 $this->_storage->sdk_version = $this->version;
892
893 if ( empty( $this->_storage->sdk_last_version ) ||
894 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
895 ) {
896 $this->_storage->sdk_upgrade_mode = true;
897 $this->_storage->sdk_downgrade_mode = false;
898 } else {
899 $this->_storage->sdk_downgrade_mode = true;
900 $this->_storage->sdk_upgrade_mode = false;
901
902 }
903
904 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
905 }
906
907 $plugin_version = $this->get_plugin_version();
908 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
909 // Plugin version upgrade mode.
910 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
911 $this->_storage->plugin_version = $plugin_version;
912
913 if ( empty( $this->_storage->plugin_last_version ) ||
914 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
915 ) {
916 $this->_storage->plugin_upgrade_mode = true;
917 $this->_storage->plugin_downgrade_mode = false;
918 } else {
919 $this->_storage->plugin_downgrade_mode = true;
920 $this->_storage->plugin_upgrade_mode = false;
921 }
922
923 if ( ! empty( $this->_storage->plugin_last_version ) ) {
924 // Different version of the plugin was installed before, therefore it's an update.
925 $this->_storage->is_plugin_new_install = false;
926 }
927
928 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
929 }
930 }
931
932 #--------------------------------------------------------------------------------
933 #region Data Migration on SDK Update
934 #--------------------------------------------------------------------------------
935
936 /**
937 * @author Vova Feldman (@svovaf)
938 * @since 1.1.5
939 *
940 * @param string $sdk_prev_version
941 * @param string $sdk_version
942 */
943 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
944 if ( empty( $sdk_prev_version ) ) {
945 return;
946 }
947
948 if (
949 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
950 version_compare( $sdk_version, '2.5.1', '>=' )
951 ) {
952 if ( $this->is_registered( true ) ) {
953 /**
954 * Migrate to new permissions layer.
955 */
956 require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
957
958 $install_by_blog_id = is_multisite() ?
959 $this->get_blog_install_map() :
960 array( 0 => $this->_site );
961
962 fs_migrate_251( $this, $install_by_blog_id );
963 }
964 }
965 }
966
967 /**
968 * @author Leo Fajardo (@leorw)
969 * @since 2.0.0
970 *
971 * @param \FS_Storage $storage
972 * @param bool|int|null $blog_id
973 */
974 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
975 if ( empty( $storage->sdk_version ) ) {
976 // New installation of the plugin, no need to upgrade.
977 return;
978 }
979
980 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
981 // Previous version is >= 2.0.0, so no need to migrate.
982 return;
983 }
984
985 // Alias.
986 $module_type = $storage->get_module_type();
987 $module_slug = $storage->get_module_slug();
988
989 $installs = self::get_all_sites( $module_type, $blog_id );
990 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
991
992 if ( ! is_object( $install ) ) {
993 return;
994 }
995
996 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
997 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
998 $install->plan_id = self::_decrypt( $install->plan->id );
999 }
1000
1001 unset( $install->plan );
1002
1003 $installs[ $module_slug ] = clone $install;
1004
1005 self::set_account_option_by_module(
1006 $module_type,
1007 'sites',
1008 $installs,
1009 true,
1010 $blog_id
1011 );
1012 }
1013 }
1014
1015 /**
1016 * @author Vova Feldman (@svovaf)
1017 * @since 1.2.2.7
1018 *
1019 * @param string $plugin_prev_version
1020 * @param string $plugin_version
1021 */
1022 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1023 if ( $this->is_theme() ) {
1024 // Expire the cache of the previous tabs since the theme may
1025 // have setting updates.
1026 $this->_cache->expire( 'tabs' );
1027 $this->_cache->expire( 'tabs_stylesheets' );
1028 }
1029 }
1030
1031 /**
1032 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1033 * - Moves some data to the network level storage.
1034 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1035 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1036 * - 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.
1037 * - 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.
1038 * - 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.
1039 *
1040 * @author Vova Feldman (@svovaf)
1041 * @since 2.0.0
1042 */
1043 private static function migrate_accounts_to_network() {
1044 $sites = self::get_sites();
1045 $sites_count = count( $sites );
1046 $connection_status = array();
1047 $plugin_slugs = array();
1048 foreach ( $sites as $site ) {
1049 $blog_id = self::get_site_blog_id( $site );
1050
1051 self::$_accounts->migrate_to_network( $blog_id );
1052
1053 /**
1054 * Build a list of all Freemius powered plugins slugs.
1055 */
1056 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1057 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1058 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1059 $plugin_slugs[ $data['slug'] ] = true;
1060 }
1061 }
1062
1063 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1064
1065 if ( is_array( $installs ) ) {
1066 foreach ( $installs as $slug => $install ) {
1067 if ( ! isset( $connection_status[ $slug ] ) ) {
1068 $connection_status[ $slug ] = array();
1069 }
1070
1071 if ( is_object( $install ) &&
1072 FS_Site::is_valid_id( $install->id ) &&
1073 FS_User::is_valid_id( $install->user_id )
1074 ) {
1075 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1076 }
1077 }
1078 }
1079 }
1080
1081 foreach ( $plugin_slugs as $slug => $true ) {
1082 if ( ! isset( $connection_status[ $slug ] ) ) {
1083 $connection_status[ $slug ] = array();
1084 }
1085
1086 foreach ( $sites as $site ) {
1087 $blog_id = self::get_site_blog_id( $site );
1088
1089 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1090 continue;
1091 }
1092
1093 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1094
1095 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1096
1097 if ( ! is_null( $is_anonymous ) ) {
1098 // Since 1.1.3 is_anonymous is an array.
1099 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1100 $is_anonymous = $is_anonymous['is'];
1101 }
1102
1103 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1104 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1105 }
1106 }
1107
1108 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1109 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1110 }
1111 }
1112 }
1113
1114 $super_admins = array();
1115
1116 foreach ( $connection_status as $slug => $blogs_status ) {
1117 $skips = 0;
1118 $ignores = 0;
1119 $connections = 0;
1120 $opted_in_users = array();
1121 $opted_in_super_admins = array();
1122
1123 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1124
1125 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1126 if ( 'skipped' === $status_or_user_id ) {
1127 $skips ++;
1128 } else if ( 'ignored' === $status_or_user_id ) {
1129 $ignores ++;
1130 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1131 $connections ++;
1132
1133 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1134 $opted_in_users[ $status_or_user_id ] = array();
1135 }
1136
1137 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1138
1139 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1140 self::is_super_admin( $status_or_user_id )
1141 ) {
1142 // Cache super-admin data.
1143 $super_admins[ $status_or_user_id ] = true;
1144
1145 // Remember opted-in super-admins for the plugin.
1146 $opted_in_super_admins[ $status_or_user_id ] = true;
1147 }
1148 }
1149 }
1150
1151 $main_super_admin_user_id = null;
1152 $all_migrated = false;
1153 if ( $sites_count == $skips ) {
1154 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1155 $storage->is_anonymous_ms = $storage->is_anonymous;
1156
1157 $all_migrated = true;
1158 } else if ( $sites_count == $ignores ) {
1159 // Don't do anything, still in activation mode.
1160
1161 $all_migrated = true;
1162 } else if ( 0 < count( $opted_in_super_admins ) ) {
1163 // Find the super-admin with the majority of installs.
1164 $max_installs_by_super_admin = 0;
1165 foreach ( $opted_in_super_admins as $user_id => $true ) {
1166 $installs_count = count( $opted_in_users[ $user_id ] );
1167
1168 if ( $installs_count > $max_installs_by_super_admin ) {
1169 $max_installs_by_super_admin = $installs_count;
1170 $main_super_admin_user_id = $user_id;
1171 }
1172 }
1173
1174 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1175 // Super-admin opted-in for all sites in the network.
1176 $storage->is_network_connected = true;
1177
1178 $all_migrated = true;
1179 }
1180
1181 // Store network user.
1182 $storage->network_user_id = $main_super_admin_user_id;
1183
1184 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1185 // Since all sites are opted-in, associating with the main site.
1186 get_current_blog_id() :
1187 // Associating with the 1st found opted-in site.
1188 $opted_in_users[ $main_super_admin_user_id ][0];
1189
1190 /**
1191 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1192 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1193 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1194 * will be empty.
1195 */
1196 $storage->migrate_to_network();
1197 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1198 } else {
1199 // At least one opt-in. All the opt-in were created by a non-super-admin.
1200 if ( 0 == $ignores ) {
1201 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1202 $storage->store( 'is_delegated_connection', true, true );
1203
1204 $all_migrated = true;
1205 }
1206 }
1207
1208 if ( ! $all_migrated ) {
1209 /**
1210 * Delegate all sites that were:
1211 * 1) Opted-in by a user that is NOT the main-super-admin.
1212 * 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.
1213 */
1214 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1215 if ( $status_or_user_id == $main_super_admin_user_id ) {
1216 continue;
1217 }
1218
1219 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1220 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1221 ) {
1222 $storage->store( 'is_delegated_connection', true, $blog_id );
1223 }
1224 }
1225 }
1226
1227
1228 if ( ( $connections + $skips > 0 ) ) {
1229 if ( $ignores > 0 ) {
1230 /**
1231 * If admin already opted-in or skipped in any of the network sites, and also
1232 * have sites which the connection decision was not yet taken, set this plugin
1233 * into network activation mode so the super-admin can choose what to do with
1234 * the rest of the sites.
1235 */
1236 self::set_network_upgrade_mode( $storage );
1237 }
1238 }
1239 }
1240 }
1241
1242 /**
1243 * Set a module into network upgrade mode.
1244 *
1245 * @author Vova Feldman (@svovaf)
1246 * @since 2.0.0
1247 *
1248 * @param \FS_Storage $storage
1249 *
1250 * @return bool
1251 */
1252 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1253 return $storage->is_network_activation = true;
1254 }
1255
1256 /**
1257 * Will return true after upgrading to the SDK with the network level integration,
1258 * when the super-admin involvement is required regarding the rest of the sites.
1259 *
1260 * @author Vova Feldman (@svovaf)
1261 * @since 2.0.0
1262 *
1263 * @return bool
1264 */
1265 function is_network_upgrade_mode() {
1266 return $this->_storage->get( 'is_network_activation' );
1267 }
1268
1269 /**
1270 * Clear flag after the upgrade mode completion.
1271 *
1272 * @author Vova Feldman (@svovaf)
1273 * @since 2.0.0
1274 *
1275 * @return bool True if network activation was on and now completed.
1276 */
1277 private function network_upgrade_mode_completed() {
1278 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1279 $this->_storage->remove( 'is_network_activation' );
1280
1281 return true;
1282 }
1283
1284 return false;
1285 }
1286
1287 #endregion
1288
1289 /**
1290 * This action is connected to the 'plugins_loaded' hook and helps to determine
1291 * if this is a new plugin installation or a plugin update.
1292 *
1293 * There are 3 different use-cases:
1294 * 1) New plugin installation right with Freemius:
1295 * 1.1 _activate_plugin_event_hook() will be executed first
1296 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1297 * and $this->_storage->plugin_last_version is not set,
1298 * $this->_storage->is_plugin_new_install will be set to TRUE.
1299 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1300 * be already set to TRUE.
1301 *
1302 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1303 * 2.1 _activate_plugin_event_hook() will not be executed, because
1304 * the activation hook do NOT fires on updates since WP 3.1.
1305 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1306 * be empty, therefore, it will be set to FALSE.
1307 *
1308 * 3) Plugin update, had Freemius in prev version as well:
1309 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1310 * before, $this->_storage->plugin_last_version will NOT be empty,
1311 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1312 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1313 * already set, therefore, it will not be modified.
1314 *
1315 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1316 *
1317 * NOTE:
1318 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1319 * and then, the next immediate PageView is the plugin's main settings page, it will not
1320 * show the opt-in right away. The reason it will happen is because Freemius execution
1321 * will be turned off till the plugin is fully loaded at least once
1322 * (till $this->_storage->was_plugin_loaded is TRUE).
1323 *
1324 * @author Vova Feldman (@svovaf)
1325 * @since 1.1.9
1326 *
1327 */
1328 function _plugins_loaded() {
1329 // Update flag that plugin was loaded with Freemius at least once.
1330 $this->_storage->was_plugin_loaded = true;
1331
1332 /**
1333 * Bug fix - only set to false when it's a plugin, due to the
1334 * execution sequence of the theme hooks and our methods, if
1335 * this will be set for themes, Freemius will always assume
1336 * it's a theme update.
1337 *
1338 * @author Vova Feldman (@svovaf)
1339 * @since 1.2.2.2
1340 */
1341 if ( $this->is_plugin() &&
1342 ! isset( $this->_storage->is_plugin_new_install )
1343 ) {
1344 $this->_storage->is_plugin_new_install = (
1345 ! is_plugin_active( $this->_plugin_basename ) &&
1346 empty( $this->_storage->plugin_last_version )
1347 );
1348 }
1349 }
1350
1351 /**
1352 * Opens the support forum subemenu item in a new browser page.
1353 *
1354 * @author Vova Feldman (@svovaf)
1355 * @since 2.1.4
1356 */
1357 static function _open_support_forum_in_new_page() {
1358 ?>
1359 <script type="text/javascript">
1360 (function ($) {
1361 $('.fs-submenu-item.wp-support-forum').parent().attr( { target: '_blank', rel: 'noopener noreferrer' } );
1362 })(jQuery);
1363 </script>
1364 <?php
1365 }
1366
1367 /**
1368 * @author Vova Feldman (@svovaf)
1369 * @since 1.0.9
1370 */
1371 private function register_constructor_hooks() {
1372 $this->_logger->entrance();
1373
1374 if ( is_admin() ) {
1375 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1376
1377 if ( $this->is_plugin() ) {
1378 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1379 /**
1380 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1381 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1382 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1383 * updating of a .org plugin).
1384 */
1385 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1386 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1387 /**
1388 * 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.
1389 *
1390 * @author Leo Fajardo (@leorw)
1391 * @since 2.2.3
1392 */
1393 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1394 }
1395
1396 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1397
1398 /**
1399 * @since 1.2.2
1400 *
1401 * Hook to both free and premium version activations to support
1402 * auto deactivation on the other version activation.
1403 */
1404 register_activation_hook(
1405 $plugin_dir . $this->_free_plugin_basename,
1406 array( &$this, '_activate_plugin_event_hook' )
1407 );
1408
1409 register_activation_hook(
1410 $plugin_dir . $this->premium_plugin_basename(),
1411 array( &$this, '_activate_plugin_event_hook' )
1412 );
1413 } else {
1414 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1415
1416 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1417 }
1418
1419 /**
1420 * Part of the mechanism to identify new plugin install vs. plugin update.
1421 *
1422 * @author Vova Feldman (@svovaf)
1423 * @since 1.1.9
1424 */
1425 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1426 /**
1427 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1428 * when the logic gets here since the activation logic first add the activate plugins,
1429 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1430 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1431 * plugin activation, and that IS intentional.
1432 *
1433 * @author Vova Feldman (@svovaf)
1434 */
1435 if ( $this->is_plugin() &&
1436 $this->is_activation_mode( false ) &&
1437 0 == did_action( 'plugins_loaded' )
1438 ) {
1439 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1440 } else {
1441 // If was activated before, then it was already loaded before.
1442 $this->_plugins_loaded();
1443 }
1444 }
1445
1446 if ( ! self::is_ajax() ) {
1447 if ( ! $this->is_addon() ) {
1448 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1449 }
1450 }
1451
1452 if ( $this->_storage->handle_gdpr_admin_notice ) {
1453 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1454 }
1455
1456 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1457 add_action( 'init', array( &$this, '_maybe_add_pricing_ajax_handler' ) );
1458 }
1459
1460 if ( $this->is_plugin() ) {
1461 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1462 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1463 } else {
1464 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1465 }
1466
1467 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1468 }
1469
1470 if ( is_multisite() ) {
1471 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1472 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1473 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1474
1475 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1476 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1477 } else {
1478 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1479 }
1480
1481 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1482 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1483 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1484 }
1485
1486 if ( $this->is_theme() &&
1487 self::is_customizer() &&
1488 $this->apply_filters( 'show_customizer_upsell', true )
1489 ) {
1490 // Register customizer upsell.
1491 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1492 }
1493
1494 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1495
1496 if ( $this->is_theme() && ! $this->is_migration() ) {
1497 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1498 }
1499
1500 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1501 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1502 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1503 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1504 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1505
1506 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1507 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1508 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1509 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1510
1511 if ( $this->_is_network_active && fs_is_network_admin() ) {
1512 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1513 }
1514
1515 $this->add_ajax_action( 'install_premium_version', array(
1516 &$this,
1517 '_install_premium_version_ajax_action'
1518 ) );
1519
1520 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1521
1522 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1523
1524 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1525
1526 $this->add_action(
1527 'plugin_version_update',
1528 array( &$this, '_after_version_update' ),
1529 WP_FS__DEFAULT_PRIORITY,
1530 2
1531 );
1532 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1533
1534 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
1535 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
1536 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1537
1538 /**
1539 * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
1540 *
1541 * @author Vova Feldman (@svovaf)
1542 * @since 1.2.1.5
1543 */
1544 if (
1545 (
1546 fs_request_is_action( 'reset_anonymous_mode' ) ||
1547 fs_request_is_action( 'reset_pending_activation_mode' )
1548 ) &&
1549 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
1550 ) {
1551 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1552 }
1553 }
1554
1555 /**
1556 * Register the required hooks right after the settings parse is completed.
1557 *
1558 * @author Vova Feldman (@svovaf)
1559 * @since 2.3.1
1560 */
1561 private function register_after_settings_parse_hooks() {
1562 if ( is_admin() &&
1563 $this->is_theme() &&
1564 $this->is_premium() &&
1565 ! $this->has_active_valid_license()
1566 ) {
1567 $this->add_ajax_action(
1568 'delete_theme_update_data',
1569 array( &$this, '_delete_theme_update_data_action' )
1570 );
1571 }
1572
1573 if ( $this->show_settings_with_tabs() ) {
1574 /**
1575 * Include the required hooks to capture the theme settings' page tabs
1576 * and cache them.
1577 *
1578 * @author Vova Feldman (@svovaf)
1579 * @since 1.2.2.7
1580 */
1581 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1582 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1583 // Add license activation AJAX callback.
1584 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1585
1586 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1587 }
1588
1589 add_action(
1590 'admin_footer',
1591 array( &$this, '_add_freemius_tabs' ),
1592 /**
1593 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1594 * That's why the priority is 11 while the tabs capture logic is added
1595 * with priority 10.
1596 *
1597 * @author Vova Feldman (@svovaf)
1598 */
1599 11
1600 );
1601 }
1602
1603 if ( ! self::is_ajax() ) {
1604 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1605 add_action(
1606 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1607 array( &$this, '_prepare_admin_menu' ),
1608 WP_FS__LOWEST_PRIORITY
1609 );
1610 }
1611 }
1612 }
1613
1614 /**
1615 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1616 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1617 *
1618 * @author Leo Fajardo (@leorw)
1619 * @since 2.2.3
1620 *
1621 * @param object $updates
1622 * @param string|null $transient
1623 *
1624 * @return object
1625 */
1626 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1627 if ( is_object( $updates ) && isset( $updates->response ) ) {
1628 foreach ( $updates->response as $file => $plugin ) {
1629 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1630 unset( $updates->response[ $file ] );
1631 }
1632 }
1633 }
1634
1635 return $updates;
1636 }
1637
1638 /**
1639 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1640 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1641 *
1642 * @author Leo Fajardo (@leorw)
1643 * @since 2.2.3
1644 *
1645 * @return string
1646 */
1647 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1648 $slug_basename_map = array();
1649 foreach ( self::$_instances as $instance ) {
1650 if ( ! $instance->is_plugin() ) {
1651 continue;
1652 }
1653
1654 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1655 }
1656 ?>
1657 <script type="text/javascript">
1658 (function( $ ) {
1659 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1660 for ( var slug in slugBasenameMap ) {
1661 var basename = slugBasenameMap[ slug ];
1662
1663 // Try to get the plugin rows if on the "Plugins" page.
1664 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1665
1666 if ( 0 === $pluginRows.length ) {
1667 // Try to get the plugin rows if on the "Updates" page.
1668 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1669 if ( 0 !== $pluginCheckbox.length ) {
1670 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1671 }
1672 }
1673
1674 if ( 0 === $pluginRows.length ) {
1675 // No plugin rows found.
1676 continue;
1677 }
1678
1679 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1680 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1681 var $this = $( this ),
1682 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1683
1684 $this.attr( 'href', href );
1685 });
1686 }
1687 })( jQuery );
1688 </script>
1689 <?php
1690 }
1691
1692 /**
1693 * @author Leo Fajardo (@leorw)
1694 * @since 2.3.0
1695 */
1696 static function _maybe_add_beta_label_styles() {
1697 $has_any_beta_version = false;
1698
1699 foreach ( self::$_instances as $instance ) {
1700 if ( $instance->is_beta() ) {
1701 $has_any_beta_version = true;
1702 break;
1703 }
1704 }
1705
1706 if ( $has_any_beta_version ) {
1707 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1708 }
1709 }
1710
1711 /**
1712 * @author Leo Fajardo (@leorw)
1713 * @since 2.3.0
1714 */
1715 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1716 $beta_data = array();
1717
1718 foreach ( self::$_instances as $instance ) {
1719 if ( ! $instance->is_premium() ) {
1720 continue;
1721 }
1722
1723 /**
1724 * If there's an available beta version update, a confirmation message will be shown when the
1725 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1726 */
1727 $has_beta_update = $instance->has_beta_update();
1728
1729 $is_beta = (
1730 // The "Beta" label is added separately for themes.
1731 $instance->is_plugin() &&
1732 $instance->is_beta()
1733 );
1734
1735 if ( ! $is_beta && ! $has_beta_update ) {
1736 continue;
1737 }
1738
1739 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1740
1741 if ( ! $has_beta_update ) {
1742 continue;
1743 }
1744
1745 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1746 '%s %s',
1747 sprintf(
1748 fs_esc_attr_inline(
1749 '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.',
1750 'beta-version-update-caution',
1751 $instance->get_slug()
1752 ),
1753 $instance->get_plugin_title()
1754 ),
1755 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1756 );
1757 }
1758
1759 if ( empty( $beta_data ) ) {
1760 return;
1761 }
1762 ?>
1763 <script type="text/javascript">
1764 ( function( $ ) {
1765 var betaData = <?php echo json_encode( $beta_data ) ?>;
1766
1767 for ( var pluginBasename in betaData ) {
1768 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1769 continue;
1770 }
1771
1772 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1773 continue;
1774 }
1775
1776 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1777 if ( 0 === $parentContainer.length ) {
1778 continue;
1779 }
1780
1781 $parentContainer.find( '.plugin-title > strong:first-child').append(
1782 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1783 );
1784 }
1785
1786 setTimeout( function() {
1787 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1788 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1789 var $parentContainer = $( this ).parents( 'tr:first' );
1790 pluginBasename = ( 0 !== $parentContainer.length ) ?
1791 $parentContainer.data( 'plugin' ) :
1792 $( this ).parents( '.theme:first' ).data( 'slug' );
1793
1794 if (
1795 betaData[ pluginBasename ] &&
1796 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1797 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1798 ) {
1799 return false;
1800 }
1801 } );
1802 }, 20 );
1803 } )( jQuery );
1804 </script>
1805 <?php
1806 }
1807
1808 /**
1809 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1810 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1811 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1812 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1813 *
1814 * @author Leo Fajardo (@leorw)
1815 *
1816 * @since 1.2.0
1817 */
1818 private function unregister_uninstall_hook() {
1819 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1820 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1821 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1822
1823 update_option( 'uninstall_plugins', $uninstallable_plugins );
1824 }
1825
1826 /**
1827 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1828 *
1829 * @param bool $store_prev_path
1830 */
1831 private function clear_module_main_file_cache( $store_prev_path = true ) {
1832 if ( ! isset( $this->_storage->plugin_main_file ) ||
1833 empty( $this->_storage->plugin_main_file->path )
1834 ) {
1835 return;
1836 }
1837
1838 if ( ! $store_prev_path ) {
1839 /**
1840 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1841 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1842 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1843 *
1844 * @author Leo Fajardo (@leorw)
1845 * @since 2.2.1
1846 */
1847 unset( $this->_storage->plugin_main_file->path );
1848 } else {
1849 $plugin_main_file = clone $this->_storage->plugin_main_file;
1850
1851 // Store cached path (2nd layer cache).
1852 $plugin_main_file->prev_path = $plugin_main_file->path;
1853
1854 // Clear cached path.
1855 unset( $plugin_main_file->path );
1856
1857 $this->_storage->plugin_main_file = $plugin_main_file;
1858 }
1859
1860 /**
1861 * Clear global cached path.
1862 *
1863 * @author Leo Fajardo (@leorw)
1864 * @since 1.2.2
1865 */
1866 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1867 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1868 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1869 }
1870
1871 /**
1872 * @author Leo Fajardo (@leorw)
1873 * @since 2.0.0
1874 */
1875 function _hook_action_links_and_register_account_hooks() {
1876 if ( $this->is_migration() ) {
1877 return;
1878 }
1879
1880 if (
1881 ( self::is_plugins_page() && $this->is_plugin() ) ||
1882 ( self::is_themes_page() && $this->is_theme() ) ||
1883 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1884 ) {
1885 $this->_add_tracking_links();
1886 }
1887
1888 if ( self::is_plugins_page() && $this->is_plugin() ) {
1889 $this->hook_plugin_action_links();
1890 }
1891
1892 $this->_register_account_hooks();
1893 }
1894
1895 /**
1896 * @author Vova Feldman (@svovaf)
1897 * @since 1.0.9
1898 */
1899 private function _register_account_hooks() {
1900 if ( ! is_admin() ) {
1901 return;
1902 }
1903
1904 /**
1905 * Always show the deactivation feedback form since we added
1906 * automatic free version deactivation upon premium code activation.
1907 *
1908 * @since 1.2.1.6
1909 */
1910 $this->add_ajax_action(
1911 'submit_uninstall_reason',
1912 array( &$this, '_submit_uninstall_reason_action' )
1913 );
1914
1915 $this->add_ajax_action(
1916 'cancel_subscription_or_trial',
1917 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1918 );
1919
1920 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1921 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1922 ( $this->is_theme() && self::is_themes_page() )
1923 ) {
1924 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1925 }
1926 }
1927 }
1928
1929 /**
1930 * Leverage backtrace to find caller plugin file path.
1931 *
1932 * @param bool $is_init Is initiation sequence.
1933 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
1934 *
1935 * @return string
1936 * @since 1.0.6
1937 *
1938 * @author Vova Feldman (@svovaf)
1939 */
1940 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
1941 // Try to load the cached value of the file path.
1942 if ( isset( $this->_storage->plugin_main_file ) ) {
1943 $plugin_main_file = $this->_storage->plugin_main_file;
1944 if ( ! empty( $plugin_main_file->path ) ) {
1945 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1946 if ( file_exists( $absolute_path ) ) {
1947 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
1948 // Update cached path if not matching the actual path.
1949 $plugin_main_file->path = $main_file;
1950 $this->_storage->plugin_main_file = $plugin_main_file;
1951 }
1952
1953 return $absolute_path;
1954 }
1955 }
1956 }
1957
1958 /**
1959 * @since 1.2.1
1960 *
1961 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
1962 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
1963 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
1964 *
1965 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
1966 * when the class instantiator isn't the module.
1967 */
1968 if ( ! $is_init ) {
1969 // Fetch prev path cache.
1970 if ( isset( $this->_storage->plugin_main_file ) &&
1971 ! empty( $this->_storage->plugin_main_file->prev_path )
1972 ) {
1973 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
1974 if ( file_exists( $absolute_path ) ) {
1975 return $absolute_path;
1976 }
1977 }
1978
1979 wp_die(
1980 $this->get_text_inline( 'Freemius SDK couldn\'t find the plugin\'s main file. Please contact [email protected] with the current error.', 'failed-finding-main-path' ) .
1981 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
1982 $this->get_text_inline( 'Error', 'error' ),
1983 array( 'back_link' => true )
1984 );
1985 }
1986
1987 /**
1988 * @since 1.2.1
1989 *
1990 * Only the original instantiator that calls dynamic_init can modify the module's path.
1991 */
1992 // Find caller module.
1993 $this->_storage->plugin_main_file = (object) array(
1994 'path' => $main_file,
1995 );
1996
1997 return $this->get_absolute_path( $main_file );
1998 }
1999
2000 /**
2001 * @author Leo Fajardo (@leorw)
2002 * @since 1.2.3
2003 *
2004 * @param string $path
2005 *
2006 * @return string
2007 */
2008 private function get_relative_path( $path ) {
2009 $module_root_dir = $this->get_module_root_dir_path();
2010 if ( 0 === strpos( $path, $module_root_dir ) ) {
2011 $path = substr( $path, strlen( $module_root_dir ) );
2012 }
2013
2014 return $path;
2015 }
2016
2017 /**
2018 * @author Leo Fajardo (@leorw)
2019 * @since 1.2.3
2020 *
2021 * @param string $path
2022 * @param string|bool $module_type
2023 *
2024 * @return string
2025 */
2026 private function get_absolute_path( $path, $module_type = false ) {
2027 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2028 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2029 $path = fs_normalize_path( $module_root_dir . $path );
2030 }
2031
2032 return $path;
2033 }
2034
2035 /**
2036 * @author Leo Fajardo (@leorw)
2037 * @since 1.2.3
2038 *
2039 * @param string|bool $module_type
2040 *
2041 * @return string
2042 */
2043 private function get_module_root_dir_path( $module_type = false ) {
2044 $is_plugin = empty( $module_type ) ?
2045 $this->is_plugin() :
2046 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2047
2048 return fs_normalize_path( trailingslashit( $is_plugin ?
2049 WP_PLUGIN_DIR :
2050 get_theme_root( get_stylesheet() ) ) );
2051 }
2052
2053 /**
2054 * @author Leo Fajardo (@leorw)
2055 *
2056 * @param number $module_id
2057 * @param string $slug
2058 *
2059 * @return string Since 2.5.0 return the module's main file path.
2060 *
2061 * @since 1.2.2
2062 */
2063 private function store_id_slug_type_path_map( $module_id, $slug ) {
2064 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2065
2066 $store_option = false;
2067
2068 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2069 $id_slug_type_path_map[ $module_id ] = array(
2070 'slug' => $slug
2071 );
2072
2073 $store_option = true;
2074 } else if (
2075 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2076 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2077 ) {
2078 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2079 $store_option = true;
2080 }
2081
2082 $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
2083
2084 if ( ! $find_caller ) {
2085 /**
2086 * This verification is for cases when suddenly the same module
2087 * is installed but with a different folder name.
2088 *
2089 * @author Vova Feldman (@svovaf)
2090 * @since 1.2.3
2091 */
2092 $find_caller = ! file_exists( $this->get_absolute_path(
2093 $id_slug_type_path_map[ $module_id ]['path'],
2094 $id_slug_type_path_map[ $module_id ]['type']
2095 ) );
2096 }
2097
2098 foreach ( $id_slug_type_path_map as $id => $data ) {
2099 if ( empty( $id ) ) {
2100 // Remove maps with empty module ID.
2101 unset( $id_slug_type_path_map[ $id ] );
2102 $store_option = true;
2103 continue;
2104 }
2105
2106 /**
2107 * 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.
2108 *
2109 * @author Vova Feldman (@svovaf)
2110 * @since 2.5.0
2111 */
2112 if ( ! $find_caller ) {
2113 if ( $id == $module_id ) {
2114 continue;
2115 }
2116
2117 if (
2118 isset( $data['path'] ) &&
2119 $data['path'] === $id_slug_type_path_map[ $module_id ]['path']
2120 ) {
2121 $find_caller = true;
2122 }
2123 }
2124 }
2125
2126 if ( $find_caller ) {
2127 $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
2128
2129 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2130 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2131
2132 $store_option = true;
2133 }
2134
2135 if ( $store_option ) {
2136 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2137 }
2138
2139 return $id_slug_type_path_map[ $module_id ]['path'];
2140 }
2141
2142 /**
2143 * Identifies the caller type: plugin or theme.
2144 *
2145 * @author Leo Fajardo (@leorw)
2146 * @since 1.2.2
2147 *
2148 * @author Vova Feldman (@svovaf)
2149 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2150 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2151 * SDK an internal file instead of directly from functions.php.
2152 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2153 *
2154 * @param number $module_id @since 2.5.0
2155 */
2156 private function get_caller_main_file_and_type( $module_id ) {
2157 self::require_plugin_essentials();
2158
2159 $all_plugins = fs_get_plugins( true );
2160 $all_plugins_paths = array();
2161
2162 // Get active plugin's main files real full names (might be symlinks).
2163 foreach ( $all_plugins as $relative_path => $data ) {
2164 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2165 /**
2166 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2167 * can't really include the SDK.
2168 *
2169 * @author Vova Feldman
2170 * @since 1.2.1.7
2171 */
2172 continue;
2173 }
2174
2175 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2176 }
2177
2178 $caller_file_candidate = false;
2179 $caller_map = array();
2180 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2181 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2182 $plugin_dir_to_skip = false;
2183
2184 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2185 if ( empty( $bt[ $i ]['file'] ) ) {
2186 continue;
2187 }
2188
2189 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2190 // If file same as the prev file in the stack, skip it.
2191 continue;
2192 }
2193
2194 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2195 'do_action',
2196 'apply_filter',
2197 // The string split is stupid, but otherwise, theme check
2198 // throws info notices.
2199 'requir' . 'e_once',
2200 'requir' . 'e',
2201 'includ' . 'e_once',
2202 'includ' . 'e',
2203 'install_and_activate_plugin',
2204 'try_activate_plugin',
2205 'activate_plugin'
2206 ) )
2207 ) {
2208 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2209 /**
2210 * Store the directory of the activator plugin so that any other file that starts with it
2211 * cannot be mistakenly chosen as a candidate caller file.
2212 *
2213 * @author Leo Fajardo
2214 *
2215 * @since 2.3.0
2216 */
2217 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2218
2219 foreach ( $all_plugins_paths as $plugin_path ) {
2220 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2221 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2222 $plugin_dir_to_skip = $plugin_dir;
2223
2224 break;
2225 }
2226 }
2227 }
2228
2229 // Ignore call stack hooks and files inclusion.
2230 continue;
2231 }
2232
2233 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2234
2235 if ( ! empty( $plugin_dir_to_skip ) ) {
2236 /**
2237 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2238 *
2239 * @author Leo Fajardo
2240 *
2241 * @since 2.3.0
2242 */
2243 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2244 continue;
2245 }
2246 }
2247
2248 if ( 'functions.php' === basename( $caller_file_path ) ) {
2249 /**
2250 * 1. Assumes that theme's starting execution file is functions.php.
2251 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2252 *
2253 * @author Vova Feldman (@svovaf)
2254 * @since 1.2.2.5
2255 */
2256
2257 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2258 $module_type = WP_FS__MODULE_TYPE_THEME;
2259
2260 /**
2261 * Relative path of the theme, e.g.:
2262 * `my-theme/functions.php`
2263 *
2264 * @author Leo Fajardo (@leorw)
2265 */
2266 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2267 '/' .
2268 basename( $caller_file_path );
2269
2270 continue;
2271 }
2272 }
2273
2274 $caller_file_hash = md5( $caller_file_path );
2275
2276 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2277 foreach ( $all_plugins_paths as $plugin_path ) {
2278 if ( empty( $plugin_path ) ) {
2279 continue;
2280 }
2281
2282 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2283 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2284 break;
2285 }
2286 }
2287 }
2288
2289 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2290 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2291 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2292 }
2293 }
2294
2295 $caller_main_file_and_type = (object) array(
2296 'module_type' => $module_type,
2297 'path' => $caller_file_candidate
2298 );
2299
2300 return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
2301 }
2302
2303 #----------------------------------------------------------------------------------
2304 #region Deactivation Feedback Form
2305 #----------------------------------------------------------------------------------
2306
2307 /**
2308 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2309 * page.
2310 *
2311 * @author Vova Feldman (@svovaf)
2312 * @author Leo Fajardo (@leorw)
2313 *
2314 * @since 1.1.2
2315 */
2316 function _add_deactivation_feedback_dialog_box() {
2317 if (
2318 $this->is_clone() ||
2319 ( is_object( $this->_site ) && ! $this->is_registered() )
2320 ) {
2321 return;
2322 }
2323
2324 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2325 $this->_get_subscription_cancellation_dialog_box_template_params() :
2326 array();
2327
2328 /**
2329 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2330 */
2331 $show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
2332 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2333 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2334 } else if ( $this->is_addon() ) {
2335 /**
2336 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2337 */
2338 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2339 }
2340
2341 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2342
2343 if (
2344 empty( $subscription_cancellation_dialog_box_template_params ) &&
2345 ! $show_deactivation_feedback_form &&
2346 empty( $uninstall_confirmation_message )
2347 ) {
2348 return;
2349 }
2350
2351 $vars = array( 'id' => $this->_module_id );
2352
2353 if ( $show_deactivation_feedback_form ) {
2354 /* Check the type of user:
2355 * 1. Long-term (long-term)
2356 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2357 * 3. Short-term (short-term)
2358 */
2359 $is_long_term_user = true;
2360
2361 // Check if the site is at least 2 days old.
2362 $time_installed = $this->_storage->install_timestamp;
2363
2364 // Difference in seconds.
2365 $date_diff = time() - $time_installed;
2366
2367 // Convert seconds to days.
2368 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2369
2370 if ( $date_diff_days < 2 ) {
2371 $is_long_term_user = false;
2372 }
2373
2374 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2375
2376 if ( $is_long_term_user ) {
2377 $user_type = 'long-term';
2378 } else {
2379 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2380 $user_type = 'non-registered-and-non-anonymous-short-term';
2381 } else {
2382 $user_type = 'short-term';
2383 }
2384 }
2385
2386 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2387
2388 $vars['reasons'] = $uninstall_reasons;
2389 }
2390
2391 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2392 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2393 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2394
2395 /**
2396 * Load the HTML template for the deactivation feedback dialog box.
2397 *
2398 * @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.
2399 */
2400 fs_require_template( 'forms/deactivation/form.php', $vars );
2401 }
2402
2403 /**
2404 * @author Leo Fajardo (@leorw)
2405 * @since 1.1.2
2406 *
2407 * @param string $user_type
2408 *
2409 * @return array The uninstall reasons for the specified user type.
2410 */
2411 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2412 $module_type = $this->_module_type;
2413
2414 $internal_message_template_var = array(
2415 'id' => $this->_module_id
2416 );
2417
2418 $plan = $this->get_plan();
2419
2420 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2421 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2422 } else {
2423 $contact_support_template = '';
2424 }
2425
2426 $reason_found_better_plugin = array(
2427 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2428 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2429 'input_type' => 'textfield',
2430 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2431 );
2432
2433 $reason_temporary_deactivation = array(
2434 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2435 'text' => sprintf(
2436 $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
2437 strtolower( $this->is_plugin() ?
2438 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2439 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2440 )
2441 ),
2442 'input_type' => '',
2443 'input_placeholder' => ''
2444 );
2445
2446 $reason_other = array(
2447 'id' => self::REASON_OTHER,
2448 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2449 'input_type' => 'textfield',
2450 'input_placeholder' => ''
2451 );
2452
2453 $long_term_user_reasons = array(
2454 array(
2455 'id' => self::REASON_NO_LONGER_NEEDED,
2456 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2457 'input_type' => '',
2458 'input_placeholder' => ''
2459 ),
2460 $reason_found_better_plugin,
2461 array(
2462 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2463 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2464 'input_type' => '',
2465 'input_placeholder' => ''
2466 ),
2467 array(
2468 'id' => self::REASON_BROKE_MY_SITE,
2469 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2470 'input_type' => '',
2471 'input_placeholder' => '',
2472 'internal_message' => $contact_support_template
2473 ),
2474 array(
2475 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2476 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2477 'input_type' => '',
2478 'input_placeholder' => '',
2479 'internal_message' => $contact_support_template
2480 )
2481 );
2482
2483 if ( $this->is_paying() ) {
2484 $long_term_user_reasons[] = array(
2485 'id' => self::REASON_CANT_PAY_ANYMORE,
2486 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2487 'input_type' => 'textfield',
2488 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2489 );
2490 }
2491
2492 $reason_dont_share_info = array(
2493 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2494 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2495 'input_type' => '',
2496 'input_placeholder' => ''
2497 );
2498
2499 /**
2500 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2501 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2502 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2503 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2504 * button in the opt-in form is shown/hidden).
2505 */
2506 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2507 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2508 }
2509
2510 $uninstall_reasons = array(
2511 'long-term' => $long_term_user_reasons,
2512 'non-registered-and-non-anonymous-short-term' => array(
2513 array(
2514 'id' => self::REASON_DIDNT_WORK,
2515 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2516 'input_type' => '',
2517 'input_placeholder' => ''
2518 ),
2519 $reason_dont_share_info,
2520 $reason_found_better_plugin
2521 ),
2522 'short-term' => array(
2523 array(
2524 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2525 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2526 'input_type' => '',
2527 'input_placeholder' => '',
2528 'internal_message' => $contact_support_template
2529 ),
2530 $reason_found_better_plugin,
2531 array(
2532 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2533 '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 ),
2534 'input_type' => 'textarea',
2535 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2536 ),
2537 array(
2538 'id' => self::REASON_NOT_WORKING,
2539 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2540 'input_type' => 'textarea',
2541 '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' )
2542 ),
2543 array(
2544 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2545 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2546 'input_type' => 'textarea',
2547 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2548 ),
2549 array(
2550 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2551 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2552 'input_type' => 'textarea',
2553 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2554 )
2555 )
2556 );
2557
2558 // Randomize the reasons for the current user type.
2559 shuffle( $uninstall_reasons[ $user_type ] );
2560
2561 // Keep the following reasons as the last items in the list.
2562 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2563 $uninstall_reasons[ $user_type ][] = $reason_other;
2564
2565 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2566
2567 return $uninstall_reasons[ $user_type ];
2568 }
2569
2570 /**
2571 * Called after the user has submitted his reason for deactivating the plugin.
2572 *
2573 * @author Leo Fajardo (@leorw)
2574 * @since 1.1.2
2575 */
2576 function _submit_uninstall_reason_action() {
2577 $this->_logger->entrance();
2578
2579 $this->check_ajax_referer( 'submit_uninstall_reason' );
2580
2581 $reason_id = fs_request_get( 'reason_id' );
2582
2583 // Check if the given reason ID is an unsigned integer.
2584 if ( ! ctype_digit( $reason_id ) ) {
2585 exit;
2586 }
2587
2588 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2589 if ( ! empty( $reason_info ) ) {
2590 $reason_info = substr( $reason_info, 0, 128 );
2591 }
2592
2593 $reason = (object) array(
2594 'id' => $reason_id,
2595 'info' => $reason_info,
2596 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2597 );
2598
2599 $this->_storage->store( 'uninstall_reason', $reason );
2600
2601 if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) {
2602 $snooze_period = fs_request_get( 'snooze_period' );
2603
2604 if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) {
2605 self::snooze_deactivation_form( (int) $snooze_period );
2606 }
2607 }
2608
2609 /**
2610 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2611 * not support uninstall hook.
2612 *
2613 * @author Leo Fajardo (@leorw)
2614 * @since 1.2.2
2615 */
2616 if ( $this->is_theme() ) {
2617 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2618 FS_Plugin_Updater::instance( $this )->delete_update_data();
2619 }
2620
2621 $this->_uninstall_plugin_event( false );
2622 $this->remove_sdk_reference();
2623 }
2624
2625 // Print '1' for successful operation.
2626 echo 1;
2627 exit;
2628 }
2629
2630 #--------------------------------------------------------------------------------
2631 #region Deactivation Feedback Snoozing
2632 #--------------------------------------------------------------------------------
2633
2634 /**
2635 * @author Vova Feldman (@svovaf)
2636 * @since 2.4.3
2637 *
2638 * @param int $period
2639 *
2640 * @return bool True if the value was set, false otherwise.
2641 */
2642 private static function snooze_deactivation_form( $period ) {
2643 return ( 0 < $period && self::reset_deactivation_snoozing( $period ) );
2644 }
2645
2646 /**
2647 * Check if deactivation feedback form is snoozed.
2648 *
2649 * @author Vova Feldman (@svovaf)
2650 * @since 2.4.3
2651 *
2652 * @return bool
2653 */
2654 static function is_deactivation_snoozed() {
2655 $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ?
2656 get_transient( 'fs_snooze_period' ) :
2657 get_site_transient( 'fs_snooze_period' );
2658
2659
2660 return ( 'true' === $is_snoozed );
2661 }
2662
2663 /**
2664 * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period.
2665 *
2666 * @param int $period Period in seconds.
2667 *
2668 * @author Vova Feldman (@svovaf)
2669 * @since 2.4.3
2670 */
2671 private static function reset_deactivation_snoozing( $period = 0 ) {
2672 $value = ( 0 === $period ) ? null : 'true';
2673
2674 if ( ! is_multisite() || fs_is_network_admin() ) {
2675 return set_transient( 'fs_snooze_period', $value, $period );
2676 } else {
2677 return set_site_transient( 'fs_snooze_period', $value, $period );
2678 }
2679 }
2680
2681 /**
2682 * The deactivation snooze expiration UNIX timestamp (in sec).
2683 *
2684 * @author Vova Feldman (@svovaf)
2685 * @since 2.4.3
2686 *
2687 * @return int
2688 */
2689 static function deactivation_snooze_expires_at() {
2690 return ( ! is_multisite() || fs_is_network_admin() ) ?
2691 (int) get_option( '_transient_timeout_fs_snooze_period' ) :
2692 (int) get_site_option( '_site_transient_timeout_fs_snooze_period' );
2693 }
2694
2695 #endregion
2696
2697 /**
2698 * @author Leo Fajardo (@leorw)
2699 * @since 2.1.4
2700 */
2701 function cancel_subscription_or_trial_ajax_action() {
2702 $this->_logger->entrance();
2703
2704 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2705
2706 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2707
2708 if ( $this->is_api_error( $result ) ) {
2709 $this->shoot_ajax_failure( $result->error->message );
2710 }
2711
2712 $this->shoot_ajax_success();
2713 }
2714
2715 /**
2716 * @author Leo Fajardo (@leorw)
2717 * @since 2.1.4
2718 *
2719 * @param number $plugin_id
2720 *
2721 * @return object
2722 */
2723 private function cancel_subscription_or_trial( $plugin_id ) {
2724 $fs = null;
2725 if ( $plugin_id == $this->get_id() ) {
2726 $fs = $this;
2727 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2728 $fs = self::get_instance_by_id( $plugin_id );
2729 }
2730
2731 $result = null;
2732
2733 if ( ! is_null( $fs ) ) {
2734 $result = $fs->is_paid_trial() ?
2735 $fs->_cancel_trial() :
2736 $fs->_downgrade_site();
2737 }
2738
2739 return $result;
2740 }
2741
2742 /**
2743 * @author Leo Fajardo (@leorw)
2744 * @since 2.0.2
2745 */
2746 function _delete_theme_update_data_action() {
2747 FS_Plugin_Updater::instance( $this )->delete_update_data();
2748 }
2749
2750 #endregion
2751
2752 #----------------------------------------------------------------------------------
2753 #region Instance
2754 #----------------------------------------------------------------------------------
2755
2756 /**
2757 * Main singleton instance.
2758 *
2759 * @author Vova Feldman (@svovaf)
2760 * @since 1.0.0
2761 *
2762 * @param number $module_id
2763 * @param string|bool $slug
2764 * @param bool $is_init Is initiation sequence.
2765 *
2766 * @return Freemius|false
2767 */
2768 static function instance( $module_id, $slug = false, $is_init = false ) {
2769 if ( empty( $module_id ) ) {
2770 return false;
2771 }
2772
2773 /**
2774 * 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.
2775 */
2776 self::_load_required_static();
2777
2778 if ( ! is_numeric( $module_id ) ) {
2779 if ( ! $is_init && true === $slug ) {
2780 $is_init = true;
2781 }
2782
2783 $slug = $module_id;
2784
2785 $module = FS_Plugin_Manager::instance( $slug )->get();
2786
2787 if ( is_object( $module ) ) {
2788 $module_id = $module->id;
2789 }
2790 }
2791
2792 $key = 'm_' . $module_id;
2793
2794 if ( ! isset( self::$_instances[ $key ] ) ) {
2795 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2796 }
2797
2798 return self::$_instances[ $key ];
2799 }
2800
2801 /**
2802 * @author Vova Feldman (@svovaf)
2803 * @since 1.0.6
2804 *
2805 * @param number $addon_id
2806 *
2807 * @return bool
2808 */
2809 private static function has_instance( $addon_id ) {
2810 return isset( self::$_instances[ 'm_' . $addon_id ] );
2811 }
2812
2813 /**
2814 * @author Leo Fajardo (@leorw)
2815 * @since 1.2.2
2816 *
2817 * @param string|number $id_or_slug
2818 * @param string $module_type
2819 *
2820 * @return number|false
2821 */
2822 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2823 if ( is_numeric( $id_or_slug ) ) {
2824 return $id_or_slug;
2825 }
2826
2827 foreach ( self::$_instances as $instance ) {
2828 // Also check the module type since there can be a plugin and a theme with the same slug.
2829 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2830 return $instance->get_id();
2831 }
2832 }
2833
2834 return false;
2835 }
2836
2837 /**
2838 * @author Vova Feldman (@svovaf)
2839 * @since 1.0.6
2840 *
2841 * @param number $id
2842 *
2843 * @return false|Freemius
2844 */
2845 static function get_instance_by_id( $id ) {
2846 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2847 self::$_instances[ 'm_' . $id ] :
2848 false;
2849 }
2850
2851 /**
2852 *
2853 * @author Vova Feldman (@svovaf)
2854 * @since 1.0.1
2855 *
2856 * @param string $plugin_file
2857 * @param string $module_type
2858 *
2859 * @return false|Freemius
2860 */
2861 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2862 $slug = self::find_slug_by_basename( $plugin_file );
2863
2864 return ( false !== $slug ) ?
2865 self::instance( self::get_module_id( $slug, $module_type ) ) :
2866 false;
2867 }
2868
2869 /**
2870 * @author Vova Feldman (@svovaf)
2871 * @since 1.0.6
2872 *
2873 * @return false|Freemius
2874 */
2875 function get_parent_instance() {
2876 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2877 }
2878
2879 /**
2880 * @author Vova Feldman (@svovaf)
2881 * @since 1.0.6
2882 *
2883 * @param string|number $id_or_slug
2884 *
2885 * @return false|Freemius
2886 */
2887 function get_addon_instance( $id_or_slug ) {
2888 $addon_id = self::get_module_id( $id_or_slug );
2889
2890 return self::instance( $addon_id );
2891 }
2892
2893 /**
2894 * @return Freemius[]
2895 */
2896 static function _get_all_instances() {
2897 return self::$_instances;
2898 }
2899
2900 #endregion ------------------------------------------------------------------
2901
2902 /**
2903 * @author Vova Feldman (@svovaf)
2904 * @since 1.0.6
2905 *
2906 * @return bool
2907 */
2908 function is_parent_plugin_installed() {
2909 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2910
2911 if ( $is_active ) {
2912 return true;
2913 }
2914
2915 /**
2916 * Parent module might be a theme. If that's the case, the add-on's FS
2917 * instance will be loaded prior to the theme's FS instance, therefore,
2918 * we need to check if it's active with a "look ahead".
2919 *
2920 * @author Vova Feldman
2921 * @since 1.2.2.3
2922 */
2923 global $fs_active_plugins;
2924 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2925 $active_theme = wp_get_theme();
2926
2927 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2928 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2929 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2930 // Parent module is a theme and it's currently active.
2931 return true;
2932 }
2933 }
2934 }
2935 }
2936
2937 return false;
2938 }
2939
2940 /**
2941 * Check if add-on parent plugin in activation mode.
2942 *
2943 * @author Vova Feldman (@svovaf)
2944 * @since 1.0.7
2945 *
2946 * @return bool
2947 */
2948 function is_parent_in_activation() {
2949 $parent_fs = $this->get_parent_instance();
2950 if ( ! is_object( $parent_fs ) ) {
2951 return false;
2952 }
2953
2954 return ( $parent_fs->is_activation_mode() );
2955 }
2956
2957 /**
2958 * Is plugin in activation mode.
2959 *
2960 * @author Vova Feldman (@svovaf)
2961 * @since 1.0.7
2962 *
2963 * @param bool $and_on
2964 *
2965 * @return bool
2966 */
2967 function is_activation_mode( $and_on = true ) {
2968 return fs_is_network_admin() ?
2969 $this->is_network_activation_mode( $and_on ) :
2970 $this->is_site_activation_mode( $and_on );
2971 }
2972
2973 /**
2974 * Is plugin in activation mode.
2975 *
2976 * @author Vova Feldman (@svovaf)
2977 * @since 1.0.7
2978 *
2979 * @param bool $and_on
2980 *
2981 * @return bool
2982 */
2983 function is_site_activation_mode( $and_on = true ) {
2984 return (
2985 ( $this->is_on() || ! $and_on ) &&
2986 (
2987 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
2988 (
2989 ( ! $this->is_registered() ||
2990 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
2991 ( ! $this->is_enable_anonymous() ||
2992 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
2993 )
2994 )
2995 );
2996 }
2997
2998 /**
2999 * Checks if the SDK in network activation mode.
3000 *
3001 * @author Leo Fajardo (@leorw)
3002 * @since 2.0.0
3003 *
3004 * @param bool $and_on
3005 *
3006 * @return bool
3007 */
3008 private function is_network_activation_mode( $and_on = true ) {
3009 if ( ! $this->_is_network_active ) {
3010 // Not network activated.
3011 return false;
3012 }
3013
3014 if ( $this->is_network_upgrade_mode() ) {
3015 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3016 return true;
3017 }
3018
3019 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3020 // 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.
3021 return false;
3022 }
3023
3024 if ( $this->is_network_delegated_connection() ) {
3025 // Super-admin delegated the connection to the site admins -> not activation mode.
3026 return false;
3027 }
3028
3029 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3030 // Super-admin skipped the connection network wide -> not activation mode.
3031 return false;
3032 }
3033
3034 if ( $this->is_network_registered() ) {
3035 // Super-admin connected at least one site -> not activation mode.
3036 return false;
3037 }
3038
3039 return true;
3040 }
3041
3042 /**
3043 * Check if current page is the opt-in/pending-activation page.
3044 *
3045 * @author Vova Feldman (@svovaf)
3046 * @since 1.2.1.7
3047 *
3048 * @return bool
3049 */
3050 function is_activation_page() {
3051 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3052 return true;
3053 }
3054
3055 if ( ! $this->is_activation_mode() ) {
3056 return false;
3057 }
3058
3059 // Check if current page is matching the activation page.
3060 return $this->is_matching_url( $this->get_activation_url() );
3061 }
3062
3063 /**
3064 * Check if URL path's are matching and that all querystring
3065 * arguments of the $sub_url exist in the $url with the same values.
3066 *
3067 * WARNING:
3068 * 1. This method doesn't check if the sub/domain are matching.
3069 * 2. Ignore case sensitivity.
3070 *
3071 * @author Vova Feldman (@svovaf)
3072 * @since 1.2.1.7
3073 *
3074 * @param string $sub_url
3075 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3076 *
3077 * @return bool
3078 */
3079 private function is_matching_url( $sub_url, $url = '' ) {
3080 if ( empty( $url ) ) {
3081 $url = $_SERVER['REQUEST_URI'];
3082 }
3083
3084 $url = strtolower( $url );
3085 $sub_url = strtolower( $sub_url );
3086
3087 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3088 // Different path - DO NOT OVERRIDE PAGE.
3089 return false;
3090 }
3091
3092 $url_params = array();
3093 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
3094
3095 $sub_url_params = array();
3096 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
3097
3098 foreach ( $sub_url_params as $key => $val ) {
3099 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3100 // Not matching query string - DO NOT OVERRIDE PAGE.
3101 return false;
3102 }
3103 }
3104
3105 return true;
3106 }
3107
3108 /**
3109 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3110 *
3111 * @author Vova Feldman (@svovaf)
3112 * @since 2.0.0
3113 *
3114 * @param int $blog_id
3115 *
3116 * @return string[]
3117 */
3118 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3119 if ( is_multisite() && $blog_id > 0 ) {
3120 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3121 } else {
3122 $active_basenames = get_option( 'active_plugins' );
3123 }
3124
3125 if ( ! is_array( $active_basenames ) ) {
3126 $active_basenames = array();
3127 }
3128
3129 if ( is_multisite() ) {
3130 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3131
3132 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3133 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3134 }
3135 }
3136
3137 return $active_basenames;
3138 }
3139
3140 /**
3141 * @author Leo Fajardo (@leorw)
3142 * @since 2.3.0
3143 *
3144 * @param int $blog_id
3145 *
3146 * @return array
3147 */
3148 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3149 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3150
3151 $map = array();
3152
3153 foreach ( $active_basenames as $active_basename ) {
3154 $active_basename = fs_normalize_path( $active_basename );
3155
3156 if ( false === strpos( $active_basename, '/' ) ) {
3157 continue;
3158 }
3159
3160 $map[ dirname( $active_basename ) ] = true;
3161 }
3162
3163 return $map;
3164 }
3165
3166 /**
3167 * Get collection of all active plugins. Including network activated plugins.
3168 *
3169 * @author Vova Feldman (@svovaf)
3170 * @since 1.0.9
3171 *
3172 * @param int $blog_id Since 2.0.0
3173 *
3174 * @return array[string]array
3175 */
3176 private static function get_active_plugins( $blog_id = 0 ) {
3177 self::require_plugin_essentials();
3178
3179 $active_plugin = array();
3180 $all_plugins = fs_get_plugins();
3181 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3182
3183 foreach ( $active_plugins_basenames as $plugin_basename ) {
3184 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3185 }
3186
3187 return $active_plugin;
3188 }
3189
3190 /**
3191 * Get collection of all site active plugins for a specified blog.
3192 *
3193 * @author Vova Feldman (@svovaf)
3194 * @since 2.0.0
3195 *
3196 * @param int $blog_id
3197 *
3198 * @return array[string]array
3199 */
3200 private static function get_site_active_plugins( $blog_id = 0 ) {
3201 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3202 get_blog_option( $blog_id, 'active_plugins' ) :
3203 get_option( 'active_plugins' );
3204
3205 $active = array();
3206
3207 if ( ! is_array( $active_basenames ) ) {
3208 return $active;
3209 }
3210
3211 foreach ( $active_basenames as $basename ) {
3212 $active[ $basename ] = array(
3213 'is_active' => true,
3214 'Version' => '1.0', // Dummy version.
3215 'slug' => self::get_plugin_slug( $basename ),
3216 );
3217 }
3218
3219 return $active;
3220 }
3221
3222 /**
3223 * Get collection of all plugins with their activation status for a specified blog.
3224 *
3225 * @author Vova Feldman (@svovaf)
3226 * @since 1.1.8
3227 *
3228 * @param int $blog_id Since 2.0.0
3229 *
3230 * @return array Key is the plugin file path and the value is an array of the plugin data.
3231 */
3232 private static function get_all_plugins( $blog_id = 0 ) {
3233 self::require_plugin_essentials();
3234
3235 $all_plugins = fs_get_plugins();
3236
3237 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3238
3239 foreach ( $all_plugins as $basename => &$data ) {
3240 // By default set to inactive (next foreach update the active plugins).
3241 $data['is_active'] = false;
3242 // Enrich with plugin slug.
3243 $data['slug'] = self::get_plugin_slug( $basename );
3244 }
3245
3246 // Flag active plugins.
3247 foreach ( $active_plugins_basenames as $basename ) {
3248 if ( isset( $all_plugins[ $basename ] ) ) {
3249 $all_plugins[ $basename ]['is_active'] = true;
3250 }
3251 }
3252
3253 return $all_plugins;
3254 }
3255
3256 /**
3257 * Get collection of all plugins and if they are network level activated.
3258 *
3259 * @author Vova Feldman (@svovaf)
3260 * @since 2.0.0
3261 *
3262 * @return array Key is the plugin basename and the value is an array of the plugin data.
3263 */
3264 private static function get_network_plugins() {
3265 self::require_plugin_essentials();
3266
3267 $all_plugins = fs_get_plugins();
3268
3269 $network_active_basenames = is_multisite() ?
3270 get_site_option( 'active_sitewide_plugins' ) :
3271 array();
3272
3273 foreach ( $all_plugins as $basename => &$data ) {
3274 // By default set to inactive (next foreach update the active plugins).
3275 $data['is_active'] = false;
3276 // Enrich with plugin slug.
3277 $data['slug'] = self::get_plugin_slug( $basename );
3278 }
3279
3280 // Flag active plugins.
3281 foreach ( $network_active_basenames as $basename ) {
3282 if ( isset( $all_plugins[ $basename ] ) ) {
3283 $all_plugins[ $basename ]['is_active'] = true;
3284 }
3285 }
3286
3287 return $all_plugins;
3288 }
3289
3290 /**
3291 * Cached result of get_site_transient( 'update_plugins' )
3292 *
3293 * @author Vova Feldman (@svovaf)
3294 * @since 1.1.8
3295 *
3296 * @var object
3297 */
3298 private static $_plugins_info;
3299
3300 /**
3301 * Helper function to get specified plugin's slug.
3302 *
3303 * @author Vova Feldman (@svovaf)
3304 * @since 1.1.8
3305 *
3306 * @param $basename
3307 *
3308 * @return string
3309 */
3310 private static function get_plugin_slug( $basename ) {
3311 if ( ! isset( self::$_plugins_info ) ) {
3312 self::$_plugins_info = get_site_transient( 'update_plugins' );
3313 }
3314
3315 $slug = '';
3316
3317 if ( is_object( self::$_plugins_info ) ) {
3318 if ( isset( self::$_plugins_info->no_update ) &&
3319 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3320 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3321 ) {
3322 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3323 } else if ( isset( self::$_plugins_info->response ) &&
3324 isset( self::$_plugins_info->response[ $basename ] ) &&
3325 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3326 ) {
3327 $slug = self::$_plugins_info->response[ $basename ]->slug;
3328 }
3329 }
3330
3331 if ( empty( $slug ) ) {
3332 // Try to find slug from FS data.
3333 $slug = self::find_slug_by_basename( $basename );
3334 }
3335
3336 if ( empty( $slug ) ) {
3337 // Fallback to plugin's folder name.
3338 $slug = dirname( $basename );
3339 }
3340
3341 return $slug;
3342 }
3343
3344 private static $_statics_loaded = false;
3345
3346 /**
3347 * Load static resources.
3348 *
3349 * @author Vova Feldman (@svovaf)
3350 * @since 1.0.1
3351 */
3352 private static function _load_required_static() {
3353 if ( self::$_statics_loaded ) {
3354 return;
3355 }
3356
3357 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3358
3359 self::$_static_logger->entrance();
3360
3361 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3362
3363 if ( is_multisite() ) {
3364 $has_skipped_migration = (
3365 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3366 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3367 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3368 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3369 );
3370
3371 /**
3372 * If the file_slug_map exists on the site level but doesn't exist on the
3373 * network level storage, it means that we need to process the storage with migration.
3374 *
3375 * 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.
3376 *
3377 * @author Vova Feldman (@svovaf)
3378 * @since 2.0.0
3379 */
3380 if (
3381 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3382 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3383 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3384 ) {
3385 self::migrate_options_to_network();
3386 }
3387 }
3388
3389 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3390
3391 if ( ! WP_FS__DEMO_MODE ) {
3392 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3393 'Freemius',
3394 '_add_debug_section'
3395 ) );
3396 }
3397
3398 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3399
3400 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3401
3402 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3403
3404 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3405
3406 if ( 0 == did_action( 'plugins_loaded' ) ) {
3407 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3408 }
3409
3410 $clone_manager = FS_Clone_Manager::instance();
3411 add_action( 'init', array( $clone_manager, '_init' ) );
3412
3413 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3414
3415 if ( self::is_plugins_page() || self::is_themes_page() ) {
3416 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3417
3418 /**
3419 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3420 * page.
3421 *
3422 * @author Leo Fajardo (@leorw)
3423 * @since 2.3.0
3424 */
3425 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3426 }
3427
3428 self::$_statics_loaded = true;
3429 }
3430
3431 #--------------------------------------------------------------------------------
3432 #region Clone
3433 #--------------------------------------------------------------------------------
3434
3435 /**
3436 * @author Leo Fajardo (@leorw)
3437 * @since 2.5.0
3438 *
3439 * @param bool $only_if_manual_resolution_is_not_hidden
3440 *
3441 * @return bool
3442 */
3443 private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3444 if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) {
3445 return false;
3446 }
3447
3448 return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired();
3449 }
3450
3451 /**
3452 * @author Leo Fajardo (@leorw)
3453 * @since 2.5.0
3454 *
3455 * @param bool $only_if_manual_resolution_is_not_hidden
3456 */
3457 function is_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3458 if ( ! is_object( $this->_site ) ) {
3459 return false;
3460 }
3461
3462 $blog_id = null;
3463
3464 if (
3465 fs_is_network_admin() &&
3466 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
3467 ) {
3468 // Ensure that we're comparing the network install's URL with the relevant subsite's URL.
3469 $blog_id = $this->_storage->network_install_blog_id;
3470 }
3471
3472 $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
3473
3474 if ( ! $this->_site->is_clone( $site_url ) ) {
3475 return false;
3476 }
3477
3478 return (
3479 ! $only_if_manual_resolution_is_not_hidden ||
3480 ! FS_Clone_Manager::instance()->should_hide_manual_resolution()
3481 );
3482 }
3483
3484 /**
3485 * @author Leo Fajardo (@leorw)
3486 * @since 2.5.0
3487 *
3488 * @param int|null $blog_id
3489 * @param bool $strip_protocol
3490 * @param bool $add_trailing_slash
3491 *
3492 * @return string
3493 */
3494 static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) {
3495 global $wp_filter;
3496
3497 $site_url_filters = array(
3498 'site_url' => null,
3499 'pre_option_siteurl' => null,
3500 'default_option_siteurl' => null,
3501 'option_siteurl' => null,
3502 );
3503
3504 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3505 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3506 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3507 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3508 unset( $wp_filter[ $hook_name ] );
3509 }
3510 }
3511
3512 $url = get_site_url( $blog_id );
3513
3514 // Re-attach the filters back.
3515 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3516 if ( ! empty( $site_url_filter ) ) {
3517 $wp_filter[ $hook_name ] = $site_url_filter;
3518 }
3519 }
3520
3521 if ( $strip_protocol ) {
3522 $url = fs_strip_url_protocol( $url );
3523 }
3524
3525 if ( $add_trailing_slash ) {
3526 $url = trailingslashit( $url );
3527 }
3528
3529 return $url;
3530 }
3531
3532 /**
3533 * @author Leo Fajardo (@leorw)
3534 * @since 2.5.0
3535 *
3536 * @param number $site_id
3537 */
3538 function fetch_install_by_id( $site_id ) {
3539 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3540 }
3541
3542 /**
3543 * @author Leo Fajardo (@leorw)
3544 * @since 2.5.0
3545 *
3546 * @return string|object|bool
3547 */
3548 function _handle_long_term_duplicate() {
3549 $this->_logger->entrance();
3550
3551 $this->delete_current_install( false );
3552
3553 $license_key = false;
3554
3555 if (
3556 is_object( $this->_license ) &&
3557 ! $this->_license->is_utilized(
3558 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3559 )
3560 ) {
3561 $license_key = $this->_license->secret_key;
3562 }
3563
3564 return $this->opt_in(
3565 false,
3566 false,
3567 false,
3568 $license_key,
3569 false,
3570 false,
3571 false,
3572 null,
3573 array(),
3574 false
3575 );
3576 }
3577
3578 #endregion
3579
3580 /**
3581 * @author Leo Fajardo (@leorw)
3582 *
3583 * @since 2.1.3
3584 */
3585 private static function migrate_options_to_network() {
3586 self::migrate_accounts_to_network();
3587
3588 // Migrate API options from site level to network level.
3589 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3590 $api_network_options->migrate_to_network();
3591
3592 // Migrate API cache to network level storage.
3593 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3594
3595 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3596 }
3597
3598 #----------------------------------------------------------------------------------
3599 #region Localization
3600 #----------------------------------------------------------------------------------
3601
3602 /**
3603 * Load framework's text domain.
3604 *
3605 * @author Vova Feldman (@svovaf)
3606 * @since 1.2.1
3607 */
3608 static function _load_textdomain() {
3609 if ( ! is_admin() ) {
3610 return;
3611 }
3612
3613 global $fs_active_plugins;
3614
3615 // Works both for plugins and themes.
3616 load_plugin_textdomain(
3617 'freemius',
3618 false,
3619 $fs_active_plugins->newest->sdk_path . '/languages/'
3620 );
3621 }
3622
3623 #endregion
3624
3625 #----------------------------------------------------------------------------------
3626 #region Debugging
3627 #----------------------------------------------------------------------------------
3628
3629 /**
3630 * @author Vova Feldman (@svovaf)
3631 * @since 1.0.8
3632 */
3633 static function _add_debug_section() {
3634 if ( ! is_super_admin() ) {
3635 // Add debug page only for super-admins.
3636 return;
3637 }
3638
3639 self::$_static_logger->entrance();
3640
3641 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3642
3643 if ( WP_FS__DEV_MODE ) {
3644 // Add top-level debug menu item.
3645 $hook = FS_Admin_Menu_Manager::add_page(
3646 $title,
3647 $title,
3648 'manage_options',
3649 'freemius',
3650 array( 'Freemius', '_debug_page_render' )
3651 );
3652 } else {
3653 // Add hidden debug page.
3654 $hook = FS_Admin_Menu_Manager::add_subpage(
3655 '',
3656 $title,
3657 $title,
3658 'manage_options',
3659 'freemius',
3660 array( 'Freemius', '_debug_page_render' )
3661 );
3662 }
3663
3664 if ( ! empty( $hook ) ) {
3665 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3666 }
3667 }
3668
3669 /**
3670 * @author Vova Feldman (@svovaf)
3671 * @since 1.1.7.3
3672 */
3673 static function _toggle_debug_mode() {
3674 check_admin_referer( 'fs_toggle_debug_mode' );
3675
3676 if ( ! is_super_admin() ) {
3677 return;
3678 }
3679
3680 $is_on = fs_request_get( 'is_on', false, 'post' );
3681
3682 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3683 update_option( 'fs_debug_mode', $is_on );
3684
3685 // Turn on/off storage logging.
3686 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3687 }
3688
3689 exit;
3690 }
3691
3692 /**
3693 * @author Vova Feldman (@svovaf)
3694 * @since 1.2.1.6
3695 */
3696 static function _get_debug_log() {
3697 check_admin_referer( 'fs_get_debug_log' );
3698
3699 if ( ! is_super_admin() ) {
3700 return;
3701 }
3702
3703 $limit = min( ! empty( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 200, 200 );
3704 $offset = min( ! empty( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 200, 200 );
3705
3706 $logs = FS_Logger::load_db_logs(
3707 fs_request_get( 'filters', false, 'post' ),
3708 $limit,
3709 $offset
3710 );
3711
3712 self::shoot_ajax_success( $logs );
3713 }
3714
3715 /**
3716 * @author Vova Feldman (@svovaf)
3717 * @since 1.2.1.7
3718 */
3719 static function _get_db_option() {
3720 check_admin_referer( 'fs_get_db_option' );
3721
3722 $option_name = fs_request_get( 'option_name' );
3723
3724 if ( ! is_super_admin() ||
3725 ! fs_starts_with( $option_name, 'fs_' )
3726 ) {
3727 self::shoot_ajax_failure();
3728 }
3729
3730 $value = get_option( $option_name );
3731
3732 $result = array(
3733 'name' => $option_name,
3734 );
3735
3736 if ( false !== $value ) {
3737 if ( ! is_string( $value ) ) {
3738 $value = json_encode( $value );
3739 }
3740
3741 $result['value'] = $value;
3742 }
3743
3744 self::shoot_ajax_success( $result );
3745 }
3746
3747 /**
3748 * @author Vova Feldman (@svovaf)
3749 * @since 1.2.1.7
3750 */
3751 static function _set_db_option() {
3752 check_admin_referer( 'fs_set_db_option' );
3753
3754 $option_name = fs_request_get( 'option_name' );
3755
3756 if ( ! is_super_admin() ||
3757 ! fs_starts_with( $option_name, 'fs_' )
3758 ) {
3759 self::shoot_ajax_failure();
3760 }
3761
3762 $option_value = fs_request_get( 'option_value' );
3763
3764 if ( ! empty( $option_value ) ) {
3765 update_option( $option_name, $option_value );
3766 }
3767
3768 self::shoot_ajax_success();
3769 }
3770
3771 /**
3772 * @author Vova Feldman (@svovaf)
3773 * @since 1.0.8
3774 */
3775 static function _debug_page_actions() {
3776 self::_clean_admin_content_section();
3777
3778 if ( fs_request_is_action( 'restart_freemius' ) ) {
3779 check_admin_referer( 'restart_freemius' );
3780
3781 if ( ! is_multisite() ) {
3782 // Clear accounts data.
3783 self::$_accounts->clear( null, true );
3784 } else {
3785 $sites = self::get_sites();
3786 foreach ( $sites as $site ) {
3787 $blog_id = self::get_site_blog_id( $site );
3788 self::$_accounts->clear( $blog_id, true );
3789 }
3790
3791 // Clear network level storage.
3792 self::$_accounts->clear( true, true );
3793 }
3794
3795 // Clear SDK reference cache.
3796 delete_option( 'fs_active_plugins' );
3797 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3798 check_admin_referer( 'clear_updates_data' );
3799
3800 if ( ! is_multisite() ) {
3801 set_site_transient( 'update_plugins', null );
3802 set_site_transient( 'update_themes', null );
3803 } else {
3804 $current_blog_id = get_current_blog_id();
3805
3806 $sites = self::get_sites();
3807 foreach ( $sites as $site ) {
3808 switch_to_blog( self::get_site_blog_id( $site ) );
3809
3810 set_site_transient( 'update_plugins', null );
3811 set_site_transient( 'update_themes', null );
3812 }
3813
3814 switch_to_blog( $current_blog_id );
3815 }
3816 } else if ( fs_request_is_action( 'reset_deactivation_snoozing' ) ) {
3817 check_admin_referer( 'reset_deactivation_snoozing' );
3818
3819 self::reset_deactivation_snoozing();
3820 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3821 check_admin_referer( 'simulate_trial' );
3822
3823 $fs = freemius( fs_request_get( 'module_id' ) );
3824
3825 // Update SDK install to at least 24 hours before.
3826 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3827 // Unset the trial shown timestamp.
3828 unset( $fs->_storage->trial_promotion_shown );
3829 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3830 check_admin_referer( 'simulate_network_upgrade' );
3831
3832 $fs = freemius( fs_request_get( 'module_id' ) );
3833
3834 self::set_network_upgrade_mode( $fs->_storage );
3835 } else if ( fs_request_is_action( 'delete_install' ) ) {
3836 check_admin_referer( 'delete_install' );
3837
3838 self::_delete_site_by_slug(
3839 fs_request_get( 'slug' ),
3840 fs_request_get( 'module_type' ),
3841 true,
3842 fs_request_get( 'blog_id', null )
3843 );
3844 } else if ( fs_request_is_action( 'delete_user' ) ) {
3845 check_admin_referer( 'delete_user' );
3846
3847 self::delete_user( fs_request_get( 'user_id' ) );
3848 } else if ( fs_request_is_action( 'download_logs' ) ) {
3849 check_admin_referer( 'download_logs' );
3850
3851 $download_url = FS_Logger::download_db_logs(
3852 fs_request_get( 'filters', false, 'post' )
3853 );
3854
3855 if ( false === $download_url ) {
3856 wp_die( 'Oops... there was an error while generating the logs download file. Please try again and if it doesn\'t work contact [email protected].' );
3857 }
3858
3859 fs_redirect( $download_url );
3860 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3861 check_admin_referer( 'migrate_options_to_network' );
3862
3863 self::migrate_options_to_network();
3864 }
3865 }
3866
3867 /**
3868 * @author Leo Fajardo (@leorw)
3869 * @since 2.5.0
3870 *
3871 * @return array
3872 */
3873 static function get_all_modules_sites() {
3874 self::$_static_logger->entrance();
3875
3876 $sites_by_type = array(
3877 WP_FS__MODULE_TYPE_PLUGIN => array(),
3878 WP_FS__MODULE_TYPE_THEME => array(),
3879 );
3880
3881 $module_types = array_keys( $sites_by_type );
3882
3883 if ( ! is_multisite() ) {
3884 foreach ( $module_types as $type ) {
3885 $sites_by_type[ $type ] = self::get_all_sites( $type );
3886
3887 foreach ( $sites_by_type[ $type ] as $slug => $install ) {
3888 $sites_by_type[ $type ][ $slug ] = array( $install );
3889 }
3890 }
3891 } else {
3892 $sites = self::get_sites();
3893
3894 foreach ( $sites as $site ) {
3895 $blog_id = self::get_site_blog_id( $site );
3896
3897 foreach ( $module_types as $type ) {
3898 $installs = self::get_all_sites( $type, $blog_id );
3899
3900 foreach ( $installs as $slug => $install ) {
3901 if ( ! isset( $sites_by_type[ $type ][ $slug ] ) ) {
3902 $sites_by_type[ $type ][ $slug ] = array();
3903 }
3904
3905 $install->blog_id = $blog_id;
3906
3907 $sites_by_type[ $type ][ $slug ][] = $install;
3908 }
3909
3910 }
3911 }
3912 }
3913
3914 return $sites_by_type;
3915 }
3916
3917 /**
3918 * @author Vova Feldman (@svovaf)
3919 * @since 1.0.8
3920 */
3921 static function _debug_page_render() {
3922 self::$_static_logger->entrance();
3923
3924 $all_modules_sites = self::get_all_modules_sites();
3925
3926 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3927
3928 $vars = array(
3929 'plugin_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_PLUGIN ],
3930 'theme_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_THEME ],
3931 'users' => self::get_all_users(),
3932 'addons' => self::get_all_addons(),
3933 'account_addons' => self::get_all_account_addons(),
3934 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3935 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3936 );
3937
3938 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3939 fs_require_once_template( 'debug.php', $vars );
3940 }
3941
3942 #endregion
3943
3944 #----------------------------------------------------------------------------------
3945 #region Connectivity Issues
3946 #----------------------------------------------------------------------------------
3947
3948 /**
3949 * Check if Freemius should be turned on for the current plugin install.
3950 *
3951 * Note:
3952 * $this->_is_on is updated in has_api_connectivity()
3953 *
3954 * @author Vova Feldman (@svovaf)
3955 * @since 1.0.9
3956 *
3957 * @return bool
3958 */
3959 function is_on() {
3960 self::$_static_logger->entrance();
3961
3962 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
3963 return false;
3964 }
3965
3966 if ( isset( $this->_is_on ) ) {
3967 return $this->_is_on;
3968 }
3969
3970 // If already installed or pending then sure it's on :)
3971 if ( $this->is_registered() || $this->is_pending_activation() ) {
3972 $this->_is_on = true;
3973
3974 return true;
3975 }
3976
3977 return false;
3978 }
3979
3980 /**
3981 * @author Vova Feldman (@svovaf)
3982 * @since 1.1.7.3
3983 *
3984 * @param bool $flush_if_no_connectivity
3985 *
3986 * @return bool
3987 */
3988 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3989 if ( ! isset( $this->_storage->connectivity_test ) ) {
3990 // Connectivity test was never executed, or cache was cleared.
3991 return true;
3992 }
3993
3994 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3995 if ( WP_FS__IS_HTTP_REQUEST ) {
3996 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3997 // Domain changed.
3998 return true;
3999 }
4000
4001 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
4002 // Server IP changed.
4003 return true;
4004 }
4005 }
4006 }
4007
4008 if ( $this->_storage->connectivity_test['is_connected'] &&
4009 $this->_storage->connectivity_test['is_active']
4010 ) {
4011 // API connected and Freemius is active - no need to run connectivity check.
4012 return false;
4013 }
4014
4015 if ( $flush_if_no_connectivity ) {
4016 /**
4017 * If explicitly asked to flush when no connectivity - do it only
4018 * if at least 10 sec passed from the last API connectivity test.
4019 */
4020 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
4021 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
4022 }
4023
4024 /**
4025 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
4026 */
4027 $version = $this->get_plugin_version();
4028 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
4029 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
4030 return true;
4031 }
4032
4033 return false;
4034 }
4035
4036 /**
4037 * @author Leo Fajardo (@leorw)
4038 * @since 2.5.4
4039 *
4040 * @param bool $is_update
4041 *
4042 * @return bool
4043 */
4044 private function should_turn_fs_on( $is_update = true ) {
4045 if (
4046 empty( $this->_plugin->opt_in_moderation ) ||
4047 ! is_array( $this->_plugin->opt_in_moderation )
4048 ) {
4049 return true;
4050 }
4051
4052 $optin_config = $this->_plugin->opt_in_moderation;
4053
4054 if (
4055 WP_FS__IS_LOCALHOST &&
4056 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
4057 ) {
4058 return true;
4059 }
4060
4061 $optin_config_key = $is_update ?
4062 'updates' :
4063 'new';
4064
4065 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
4066 return true;
4067 }
4068
4069 $visibility_percentage = $optin_config[ $optin_config_key ];
4070
4071 if ( 0 == $visibility_percentage ) {
4072 return false;
4073 }
4074
4075 if ( ! is_numeric( $visibility_percentage ) ) {
4076 return true;
4077 }
4078
4079 $min = 1;
4080 $max = 100;
4081
4082 if ( function_exists( 'random_int' ) ) {
4083 $random = random_int( $min, $max );
4084 } else {
4085 $random = rand( $min, $max );
4086 }
4087
4088 return ( $random <= $visibility_percentage );
4089 }
4090
4091 /**
4092 * Check if there's any connectivity issue to Freemius API.
4093 *
4094 * @author Vova Feldman (@svovaf)
4095 * @since 1.0.9
4096 *
4097 * @param bool $flush_if_no_connectivity
4098 *
4099 * @return bool|null
4100 */
4101 function has_api_connectivity( $flush_if_no_connectivity = false ) {
4102 $this->_logger->entrance();
4103
4104 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
4105 return $this->_has_api_connection;
4106 }
4107
4108 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
4109 isset( $this->_storage->connectivity_test ) &&
4110 true === $this->_storage->connectivity_test['is_connected']
4111 ) {
4112 $this->clear_connectivity_info();
4113 }
4114
4115 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
4116 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
4117 /**
4118 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
4119 *
4120 * @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.
4121 */
4122 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
4123 $this->is_premium() ||
4124 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4125
4126 return $this->_has_api_connection;
4127 }
4128
4129 if (
4130 ! empty( $this->_storage->connectivity_test ) &&
4131 isset( $this->_storage->connectivity_test['is_active'] )
4132 ) {
4133 $is_active = $this->_storage->connectivity_test['is_active'];
4134 } else {
4135 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
4136
4137 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), null );
4138 }
4139
4140 if ( $is_active ) {
4141 $this->_is_on = true;
4142 }
4143
4144 return $this->_has_api_connection;
4145 }
4146
4147 /**
4148 * @author Leo Fajardo (@leorw)
4149 * @since 2.5.4
4150 */
4151 private function clear_connectivity_info() {
4152 unset( $this->_storage->connectivity_test );
4153
4154 FS_Api::clear_force_http_flag();
4155 }
4156
4157 /**
4158 * @author Vova Feldman (@svovaf)
4159 * @since 1.1.7.4
4160 *
4161 * @param object $pong
4162 * @param bool|null $is_connected
4163 */
4164 private function store_connectivity_info( $pong, $is_connected ) {
4165 $this->_logger->entrance();
4166
4167 $version = $this->get_plugin_version();
4168
4169 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
4170 $is_active = false;
4171 } else {
4172 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
4173 }
4174
4175 $is_active = $this->apply_filters(
4176 'is_on',
4177 $is_active,
4178 $this->is_plugin_update(),
4179 $version
4180 );
4181
4182 $this->_storage->connectivity_test = array(
4183 'is_connected' => $is_connected,
4184 'host' => $_SERVER['HTTP_HOST'],
4185 'server_ip' => WP_FS__REMOTE_ADDR,
4186 'is_active' => $is_active,
4187 'timestamp' => WP_FS__SCRIPT_START_TIME,
4188 // Last version with connectivity attempt.
4189 'version' => $version,
4190 );
4191
4192 $this->_has_api_connection = $is_connected;
4193 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4194 }
4195
4196 /**
4197 * @author Leo Fajardo (@leorw)
4198 * @since 2.5.4
4199 *
4200 * @param bool $is_connected
4201 */
4202 private function update_connectivity_info( $is_connected ) {
4203 $this->store_connectivity_info(
4204 // 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.
4205 (object) array( 'is_active' => true ),
4206 $is_connected
4207 );
4208 }
4209
4210 /**
4211 * Force turning Freemius on.
4212 *
4213 * @author Vova Feldman (@svovaf)
4214 * @since 1.1.8.1
4215 *
4216 * @return bool TRUE if successfully turned on.
4217 */
4218 private function turn_on() {
4219 $this->_logger->entrance();
4220
4221 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4222 return false;
4223 }
4224
4225 $updated_connectivity = $this->_storage->connectivity_test;
4226 $updated_connectivity['is_active'] = true;
4227 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4228 $this->_storage->connectivity_test = $updated_connectivity;
4229
4230 $this->_is_on = true;
4231
4232 return true;
4233 }
4234
4235 /**
4236 * Anonymous and unique site identifier (Hash).
4237 *
4238 * @author Vova Feldman (@svovaf)
4239 * @since 1.1.0
4240 *
4241 * @param null|int $blog_id Since 2.0.0
4242 *
4243 * @return string
4244 */
4245 function get_anonymous_id( $blog_id = null ) {
4246 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4247
4248 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4249 $key = self::get_unfiltered_site_url( $blog_id, true );
4250
4251 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4252 if ( empty( $secure_auth ) ||
4253 false !== strpos( $secure_auth, ' ' ) ||
4254 'put your unique phrase here' === $secure_auth
4255 ) {
4256 // Protect against default auth key.
4257 $secure_auth = md5( microtime() );
4258 }
4259
4260 /**
4261 * Base the unique identifier on the WP secure authentication key. Which
4262 * turns the key into a secret anonymous identifier. This will help us
4263 * to avoid duplicate installs generation on the backend upon opt-in.
4264 *
4265 * @author Vova Feldman (@svovaf)
4266 * @since 1.2.3
4267 */
4268 $unique_id = md5( $key . $secure_auth );
4269
4270 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4271 }
4272
4273 $this->_logger->departure( $unique_id );
4274
4275 return $unique_id;
4276 }
4277
4278 /**
4279 * Returns anonymous network ID.
4280 *
4281 * @since 2.4.3
4282 *
4283 * @return string
4284 */
4285 function get_anonymous_network_id() {
4286 return $this->get_anonymous_id( get_network()->site_id );
4287 }
4288
4289 /**
4290 * @author Vova Feldman (@svovaf)
4291 * @since 1.1.7.4
4292 *
4293 * @return \WP_User
4294 */
4295 static function _get_current_wp_user() {
4296 self::require_pluggable_essentials();
4297 self::wp_cookie_constants();
4298
4299 return wp_get_current_user();
4300 }
4301
4302 /**
4303 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4304 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4305 * is network activated the cookie constants are only configured after the network
4306 * plugins activation, therefore, if we don't define those constants WP will throw
4307 * PHP warnings/notices.
4308 *
4309 * @author Vova Feldman (@svovaf)
4310 * @since 2.1.1
4311 */
4312 private static function wp_cookie_constants() {
4313 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4314 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4315 ) {
4316 return;
4317 }
4318
4319 /**
4320 * Used to guarantee unique hash cookies
4321 *
4322 * @since 1.5.0
4323 */
4324 if ( ! defined( 'COOKIEHASH' ) ) {
4325 $siteurl = get_site_option( 'siteurl' );
4326 if ( $siteurl ) {
4327 define( 'COOKIEHASH', md5( $siteurl ) );
4328 } else {
4329 define( 'COOKIEHASH', '' );
4330 }
4331 }
4332
4333 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4334 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4335 }
4336
4337 /**
4338 * @since 2.5.0
4339 */
4340 if ( ! defined( 'AUTH_COOKIE' ) ) {
4341 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4342 }
4343
4344 /**
4345 * @since 2.6.0
4346 */
4347 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4348 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4349 }
4350 }
4351
4352 /**
4353 * @author Vova Feldman (@svovaf)
4354 * @since 2.1.0
4355 *
4356 * @return int
4357 */
4358 static function get_current_wp_user_id() {
4359 $wp_user = self::_get_current_wp_user();
4360
4361 return $wp_user->ID;
4362 }
4363
4364 /**
4365 * @author Vova Feldman (@svovaf)
4366 * @since 1.2.1.7
4367 *
4368 * @param string $email
4369 *
4370 * @return bool
4371 */
4372 static function is_valid_email( $email ) {
4373 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4374 return false;
4375 }
4376
4377 $parts = explode( '@', $email );
4378
4379 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4380 return false;
4381 }
4382
4383 $blacklist = array(
4384 'admin.',
4385 'webmaster.',
4386 'localhost.',
4387 'dev.',
4388 'development.',
4389 'test.',
4390 'stage.',
4391 'staging.',
4392 );
4393
4394 // Make sure domain is not one of the blacklisted.
4395 foreach ( $blacklist as $invalid ) {
4396 if ( 0 === strpos( $parts[1], $invalid ) ) {
4397 return false;
4398 }
4399 }
4400
4401 // Get the UTF encoded domain name.
4402 $domain = idn_to_ascii( $parts[1] ) . '.';
4403
4404 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4405 }
4406
4407 #endregion
4408
4409 #----------------------------------------------------------------------------------
4410 #region Email
4411 #----------------------------------------------------------------------------------
4412
4413 /**
4414 * Generates and sends an HTML email with customizable sections.
4415 *
4416 * @author Leo Fajardo (@leorw)
4417 * @since 1.1.2
4418 *
4419 * @param string $to_address
4420 * @param string $subject
4421 * @param array $sections
4422 * @param array $headers
4423 *
4424 * @return bool Whether the email contents were sent successfully.
4425 */
4426 private function send_email(
4427 $to_address,
4428 $subject,
4429 $sections = array(),
4430 $headers = array()
4431 ) {
4432 $default_sections = $this->get_email_sections();
4433
4434 // Insert new sections or replace the default email sections.
4435 if ( is_array( $sections ) && ! empty( $sections ) ) {
4436 foreach ( $sections as $section_id => $custom_section ) {
4437 if ( ! isset( $default_sections[ $section_id ] ) ) {
4438 // If the section does not exist, add it.
4439 $default_sections[ $section_id ] = $custom_section;
4440 } else {
4441 // If the section already exists, override it.
4442 $current_section = $default_sections[ $section_id ];
4443
4444 // Replace the current section's title if a custom section title exists.
4445 if ( isset( $custom_section['title'] ) ) {
4446 $current_section['title'] = $custom_section['title'];
4447 }
4448
4449 // Insert new rows under the current section or replace the default rows.
4450 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4451 foreach ( $custom_section['rows'] as $row_id => $row ) {
4452 $current_section['rows'][ $row_id ] = $row;
4453 }
4454 }
4455
4456 $default_sections[ $section_id ] = $current_section;
4457 }
4458 }
4459 }
4460
4461 $vars = array( 'sections' => $default_sections );
4462 $message = fs_get_template( 'email.php', $vars );
4463
4464 // Set the type of email to HTML.
4465 $headers[] = 'Content-type: text/html; charset=UTF-8';
4466
4467 $header_string = implode( "\r\n", $headers );
4468
4469 return wp_mail(
4470 $to_address,
4471 $subject,
4472 $message,
4473 $header_string
4474 );
4475 }
4476
4477 /**
4478 * Generates the data for the sections of the email content.
4479 *
4480 * @author Leo Fajardo (@leorw)
4481 * @since 1.1.2
4482 *
4483 * @return array
4484 */
4485 private function get_email_sections() {
4486 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4487 $current_user = self::_get_current_wp_user();
4488
4489 // Retrieve the cURL version information so that we can get the version number below.
4490 $curl_version_information = curl_version();
4491
4492 $active_plugin = self::get_active_plugins();
4493
4494 // Generate the list of active plugins separated by new line.
4495 $active_plugin_string = '';
4496 foreach ( $active_plugin as $plugin ) {
4497 $active_plugin_string .= sprintf(
4498 '<a href="%s">%s</a> [v%s]<br>',
4499 $plugin['PluginURI'],
4500 $plugin['Name'],
4501 $plugin['Version']
4502 );
4503 }
4504
4505 $server_ip = WP_FS__REMOTE_ADDR;
4506
4507 // Add PHP info for deeper investigation.
4508 ob_start();
4509 phpinfo();
4510 $php_info = ob_get_clean();
4511
4512 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4513
4514 // Generate the default email sections.
4515 $sections = array(
4516 'sdk' => array(
4517 'title' => 'SDK',
4518 'rows' => array(
4519 'fs_version' => array( 'FS Version', $this->version ),
4520 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4521 )
4522 ),
4523 'plugin' => array(
4524 'title' => ucfirst( $this->get_module_type() ),
4525 'rows' => array(
4526 'name' => array( 'Name', $this->get_plugin_name() ),
4527 'version' => array( 'Version', $this->get_plugin_version() )
4528 )
4529 ),
4530 'api' => array(
4531 'title' => 'API Subdomain',
4532 'rows' => array(
4533 'dns' => array(
4534 'DNS_CNAME',
4535 function_exists( 'dns_get_record' ) ?
4536 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4537 'dns_get_record() disabled/blocked'
4538 ),
4539 'ip' => array(
4540 'IP',
4541 function_exists( 'gethostbyname' ) ?
4542 gethostbyname( $api_domain ) :
4543 'gethostbyname() disabled/blocked'
4544 ),
4545 ),
4546 ),
4547 'site' => array(
4548 'title' => 'Site',
4549 'rows' => array(
4550 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4551 'address' => array( 'Address', site_url() ),
4552 'host' => array(
4553 'HTTP_HOST',
4554 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4555 ),
4556 'hosting' => array(
4557 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4558 fs_request_get( 'hosting_company' ) :
4559 'Unknown',
4560 ),
4561 'server_addr' => array(
4562 'SERVER_ADDR',
4563 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4564 )
4565 )
4566 ),
4567 'user' => array(
4568 'title' => 'User',
4569 'rows' => array(
4570 'email' => array( 'Email', $current_user->user_email ),
4571 'first' => array( 'First', $current_user->user_firstname ),
4572 'last' => array( 'Last', $current_user->user_lastname )
4573 )
4574 ),
4575 'plugins' => array(
4576 'title' => 'Plugins',
4577 'rows' => array(
4578 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4579 )
4580 ),
4581 'php_info' => array(
4582 'title' => 'PHP Info',
4583 'rows' => array(
4584 'info' => array( $php_info )
4585 ),
4586 )
4587 );
4588
4589 // Allow the sections to be modified by other code.
4590 $sections = $this->apply_filters( 'email_template_sections', $sections );
4591
4592 return $sections;
4593 }
4594
4595 #endregion
4596
4597 #----------------------------------------------------------------------------------
4598 #region Initialization
4599 #----------------------------------------------------------------------------------
4600
4601 /**
4602 * Init plugin's Freemius instance.
4603 *
4604 * @author Vova Feldman (@svovaf)
4605 * @since 1.0.1
4606 *
4607 * @param number $id
4608 * @param string $public_key
4609 * @param bool $is_live
4610 * @param bool $is_premium
4611 */
4612 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4613 $this->_logger->entrance();
4614
4615 $this->dynamic_init( array(
4616 'id' => $id,
4617 'public_key' => $public_key,
4618 'is_live' => $is_live,
4619 'is_premium' => $is_premium,
4620 ) );
4621 }
4622
4623 /**
4624 * Dynamic initiator, originally created to support initiation
4625 * with parent_id for add-ons.
4626 *
4627 * @author Vova Feldman (@svovaf)
4628 * @since 1.0.6
4629 *
4630 * @param array $plugin_info
4631 *
4632 * @throws Freemius_Exception
4633 */
4634 function dynamic_init( array $plugin_info ) {
4635 $this->_logger->entrance();
4636
4637 $this->parse_settings( $plugin_info );
4638
4639 $this->register_after_settings_parse_hooks();
4640
4641 /**
4642 * 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.
4643 *
4644 * @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.
4645 */
4646 if ( $this->is_anonymous() ) {
4647 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4648
4649 if (
4650 ! $is_network_level ||
4651 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4652 ) {
4653 if ( $this->is_paying_or_trial() ) {
4654 $this->reset_anonymous_mode( $is_network_level );
4655 }
4656 } else {
4657 $network = get_network();
4658
4659 if ( is_object( $network ) ) {
4660 $main_blog_id = $network->site_id;
4661 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4662
4663 if ( is_object( $first_install ) ) {
4664 $this->_storage->network_install_blog_id = $main_blog_id;
4665 $this->_storage->network_user_id = $first_install->user_id;
4666 }
4667 }
4668 }
4669 }
4670
4671 if ( $this->should_stop_execution() ) {
4672 return;
4673 }
4674
4675 if ( ! $this->is_registered() ) {
4676 if ( $this->is_anonymous() ) {
4677 // If user skipped, no need to test connectivity.
4678 $this->_has_api_connection = true;
4679 $this->_is_on = true;
4680 } else {
4681 if ( false === $this->has_api_connectivity() ) {
4682 return;
4683 } else {
4684 if ( $this->_anonymous_mode ) {
4685 // Simulate anonymous mode.
4686 $this->_is_anonymous = true;
4687 }
4688 }
4689 }
4690 }
4691
4692 /**
4693 * This should be executed even if Freemius is off for the core module,
4694 * otherwise, the add-ons dialog box won't work properly. This is especially
4695 * relevant when the developer decided to turn FS off for existing users.
4696 *
4697 * @author Vova Feldman (@svovaf)
4698 */
4699 if ( $this->is_user_in_admin() &&
4700 'plugin-information' === fs_request_get( 'tab', false ) &&
4701 $this->should_use_freemius_updater_and_dialog() &&
4702 (
4703 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4704 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4705 )
4706 ) {
4707 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4708
4709 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4710 }
4711
4712 // Check if Freemius is on for the current plugin.
4713 // This MUST be executed after all the plugin variables has been loaded.
4714 if ( ! $this->is_registered() && ! $this->is_on() ) {
4715 return;
4716 }
4717
4718 if ( $this->has_api_connectivity() ) {
4719 if ( self::is_cron() ) {
4720 $this->hook_callback_to_sync_cron();
4721 } else if ( $this->is_user_in_admin() ) {
4722 /**
4723 * Schedule daily data sync cron if:
4724 *
4725 * 1. User opted-in (for tracking).
4726 * 2. If skipped, but later upgraded (opted-in via upgrade).
4727 *
4728 * @author Vova Feldman (@svovaf)
4729 * @since 1.1.7.3
4730 *
4731 */
4732 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4733 $this->maybe_schedule_sync_cron();
4734 }
4735
4736 /**
4737 * Check if requested for manual blocking background sync.
4738 */
4739 if ( fs_request_has( 'background_sync' ) ) {
4740 self::require_pluggable_essentials();
4741 self::wp_cookie_constants();
4742
4743 $this->run_manual_sync();
4744 }
4745 }
4746 }
4747
4748 if ( $this->is_registered() ) {
4749 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4750
4751 $this->hook_callback_to_install_sync();
4752 }
4753
4754 if ( $this->is_addon() ) {
4755 if ( $this->is_parent_plugin_installed() ) {
4756 // Link to parent FS.
4757 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4758
4759 // Get parent plugin reference.
4760 $this->_parent_plugin = $this->_parent->get_plugin();
4761 }
4762 }
4763
4764 if ( $this->is_user_in_admin() ) {
4765 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4766 $this->_admin_notices->add_sticky(
4767 sprintf(
4768 /* translators: %s: License type (e.g. you have a professional license) */
4769 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4770 fs_request_get( 'purchased_plan' )
4771 ) .
4772 sprintf(
4773 $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' ),
4774 $this->get_module_label( true ),
4775 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4776 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4777 sprintf(
4778 '<strong>%s</strong>',
4779 fs_request_get( 'purchase_email' )
4780 )
4781 ),
4782 'plan_purchased',
4783 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4784 );
4785 }
4786
4787 if ( $this->is_addon() ) {
4788 if ( ! $this->is_parent_plugin_installed() ) {
4789 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4790
4791 if ( isset( $plugin_info['parent'] ) ) {
4792 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4793 }
4794
4795 $this->_admin_notices->add(
4796 ( ! empty( $parent_name ) ?
4797 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 ) :
4798 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() )
4799 ),
4800 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4801 'error'
4802 );
4803
4804 return;
4805 } else {
4806 $is_network_admin = fs_is_network_admin();
4807
4808 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4809 // If add-on activated and parent not, automatically install parent for the user.
4810 $this->activate_parent_account( $this->_parent );
4811 } else if (
4812 $this->_parent->is_registered() &&
4813 ! $this->is_registered() &&
4814 /**
4815 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4816 * * Network active and in network admin - network activate add-on account.
4817 * * Network active and not in network admin - activate add-on account for the current blog.
4818 * * Not network active and not in network admin - activate add-on account for the current blog.
4819 *
4820 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4821 *
4822 * @author Leo Fajardo (@leorw)
4823 * @since 2.3.0
4824 */
4825 ( $this->is_network_active() || ! $is_network_admin )
4826 ) {
4827 $premium_license = null;
4828
4829 if (
4830 ! $this->has_free_plan() &&
4831 $this->is_bundle_license_auto_activation_enabled() &&
4832 $this->_parent->is_activated_with_bundle_license()
4833 ) {
4834 /**
4835 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4836 *
4837 * @author Leo Fajardo (@leorw)
4838 * @since 2.4.0
4839 */
4840 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4841
4842 if (
4843 is_object( $bundle_license ) &&
4844 ! empty( $bundle_license->products ) &&
4845 in_array( $this->get_id(), $bundle_license->products )
4846 ) {
4847 $premium_license = $bundle_license;
4848 }
4849 }
4850
4851 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4852 // If parent plugin activated, automatically install add-on for the user.
4853 $this->_activate_addon_account(
4854 $this->_parent,
4855 ( $this->is_network_active() && $is_network_admin ) ?
4856 true :
4857 get_current_blog_id(),
4858 $premium_license
4859 );
4860 }
4861 }
4862
4863 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4864 if ( $this->is_premium() ) {
4865 // Remove add-on download admin-notice.
4866 $this->_parent->_admin_notices->remove_sticky( array(
4867 'addon_plan_upgraded_' . $this->_slug,
4868 'no_addon_license_' . $this->_slug,
4869 ) );
4870 }
4871
4872 // $this->deactivate_premium_only_addon_without_license();
4873 }
4874 }
4875
4876 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4877
4878 // if ( $this->is_registered() ||
4879 // $this->is_anonymous() ||
4880 // $this->is_pending_activation()
4881 // ) {
4882 // $this->_init_admin();
4883 // }
4884 }
4885
4886 /**
4887 * Should be called outside `$this->is_user_in_admin()` scope
4888 * because the updater has some logic that needs to be executed
4889 * during AJAX calls.
4890 *
4891 * Currently, we need to hook to the `http_request_host_is_external` filter.
4892 * In the future, there might be additional logic added.
4893 *
4894 * @author Vova Feldman
4895 * @since 1.2.1.6
4896 */
4897 if (
4898 $this->should_use_freemius_updater_and_dialog() &&
4899 (
4900 $this->is_premium() ||
4901 /**
4902 * If not premium but the premium version is installed, also instantiate the updater so that the
4903 * plugin information dialog of the premium version will have the information from the server.
4904 *
4905 * @author Leo Fajardo (@leorw)
4906 * @since 2.2.3
4907 */
4908 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4909 ) &&
4910 $this->has_release_on_freemius() &&
4911 ( ! $this->is_unresolved_clone( true ) )
4912 ) {
4913 FS_Plugin_Updater::instance( $this );
4914 }
4915
4916 $this->do_action( 'initiated' );
4917
4918 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4919 if ( isset( $this->_storage->prev_is_premium ) ) {
4920 $this->apply_filters(
4921 'after_code_type_change',
4922 // New code type.
4923 $this->_plugin->is_premium
4924 );
4925 } else {
4926 // Set for code type for the first time.
4927 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4928 }
4929 }
4930
4931 if ( ! $this->is_addon() ) {
4932 if ( $this->is_registered() ) {
4933 // Fix for upgrade from versions < 1.0.9.
4934 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4935 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4936 }
4937
4938 $this->do_action( 'after_init_plugin_registered' );
4939 } else if ( $this->is_anonymous() ) {
4940 $this->do_action( 'after_init_plugin_anonymous' );
4941 } else if ( $this->is_pending_activation() ) {
4942 $this->do_action( 'after_init_plugin_pending_activations' );
4943 }
4944 } else {
4945 if ( $this->is_registered() ) {
4946 $this->do_action( 'after_init_addon_registered' );
4947 } else if ( $this->is_anonymous() ) {
4948 $this->do_action( 'after_init_addon_anonymous' );
4949 } else if ( $this->is_pending_activation() ) {
4950 $this->do_action( 'after_init_addon_pending_activations' );
4951 }
4952 }
4953 }
4954
4955 /**
4956 * @author Leo Fajardo (@leorw)
4957 * @since 2.2.3
4958 *
4959 * @return bool
4960 */
4961 private function should_use_freemius_updater_and_dialog() {
4962 return (
4963 /**
4964 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
4965 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
4966 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
4967 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
4968 * plugin details from .org).
4969 */
4970 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
4971 (
4972 ! self::is_plugin_install_page() &&
4973 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
4974 ( 'install-plugin' !== fs_request_get( 'action' ) )
4975 )
4976 );
4977 }
4978
4979 /**
4980 * @param string[] $permissions
4981 * @param bool $is_enabled
4982 * @param int|null $blog_id
4983 *
4984 * @return true|object `true` on success, API error object on failure.
4985 */
4986 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
4987 $this->_logger->entrance();
4988
4989 $params = array(
4990 'permissions' => implode( ',', $permissions ),
4991 'is_enabled' => $is_enabled,
4992 );
4993
4994 $current_blog_id = get_current_blog_id();
4995 $is_blog_switched = false;
4996 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
4997 $is_blog_switched = $this->switch_to_blog( $blog_id );
4998 }
4999
5000 $result = $this->api_site_call( '/permissions.json', 'put', $params );
5001
5002 if ( $is_blog_switched ) {
5003 $this->switch_to_blog( $current_blog_id );
5004 }
5005
5006 if (
5007 ! $this->is_api_result_object( $result ) ||
5008 ! isset( $result->install_id )
5009 ) {
5010 $this->_logger->api_error( $result );
5011
5012 return $result;
5013 }
5014
5015 return true;
5016 }
5017
5018 /**
5019 * @param string[] $permissions
5020 * @param bool $is_enabled
5021 * @param bool $has_site_delegated_connection
5022 *
5023 * @return true|object `true` on success, API error object on failure.
5024 */
5025 private function update_network_permissions(
5026 array $permissions,
5027 $is_enabled,
5028 &$has_site_delegated_connection
5029 ) {
5030 $this->_logger->entrance();
5031
5032 $install_id_2_blog_id = array();
5033 $install_by_blog_id = $this->get_blog_install_map();
5034
5035 $has_site_delegated_connection = false;
5036
5037 foreach ( $install_by_blog_id as $blog_id => $install ) {
5038 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5039 // Only update permissions of non-delegated installs.
5040 $has_site_delegated_connection = true;
5041 continue;
5042 }
5043
5044 $install_id_2_blog_id[ $install->id ] = $blog_id;
5045 }
5046
5047 if ( empty( $install_id_2_blog_id ) ) {
5048 return true;
5049 }
5050
5051 $params = array(
5052 'permissions' => implode( ',', $permissions ),
5053 'is_enabled' => $is_enabled,
5054 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
5055 );
5056
5057 // Send update to FS.
5058 $result = $this->get_current_or_network_user_api_scope()->call(
5059 "/plugins/{$this->_module_id}/installs/permissions.json",
5060 'put',
5061 $params
5062 );
5063
5064 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
5065 $this->_logger->api_error( $result );
5066
5067 return $result;
5068 }
5069
5070 return true;
5071 }
5072
5073 /**
5074 * @param mixed $result
5075 *
5076 * @return string
5077 */
5078 private function get_api_error_message( $result ) {
5079 $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:',
5080 'unexpected-api-error' ), $this->_module_type ) . ' ';
5081
5082 if (
5083 $this->is_api_error( $result ) &&
5084 isset( $result->error )
5085 ) {
5086 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
5087
5088 $error_message .= "<b>{$result->error->message}{$code}</b>";
5089 } else {
5090 $error_message .= var_export( $result, true );
5091 }
5092
5093 return $error_message;
5094 }
5095
5096 /**
5097 * @author Vova Feldman (@svovaf)
5098 * @since 2.5.1
5099 */
5100 function _toggle_permission_tracking_callback() {
5101 $this->_logger->entrance();
5102
5103 $this->check_ajax_referer( 'toggle_permission_tracking' );
5104
5105 if ( ! $this->is_registered( true ) ) {
5106 self::shoot_ajax_failure( 'User never opted-in.' );
5107 }
5108
5109 $is_enabled = fs_request_get_bool( 'is_enabled' );
5110 $permissions = fs_request_get( 'permissions' );
5111
5112 if ( ! is_string( $permissions ) ) {
5113 self::shoot_ajax_failure( 'The permissions param must be a string.' );
5114 }
5115
5116 $permissions = explode( ',', $permissions );
5117
5118 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
5119
5120 if ( true !== $result ) {
5121 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
5122 }
5123
5124 self::shoot_ajax_success();
5125 }
5126
5127 /**
5128 * @param string[] $permissions
5129 * @param bool $is_enabled
5130 * @param int|null $blog_id
5131 *
5132 * @return bool|mixed `true` if updated successfully or no update is needed.
5133 */
5134 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
5135 if ( ! $this->is_registered( true ) ) {
5136 // User never opted-in.
5137 return false;
5138 }
5139
5140 // Check if permissions are already set as needed.
5141 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
5142 /**
5143 * Note:
5144 * 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.
5145 */
5146 return true;
5147 }
5148
5149 $api_managed_permissions = array_intersect(
5150 $permissions,
5151 FS_Permission_Manager::get_api_managed_permission_ids()
5152 );
5153
5154 if (
5155 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
5156 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
5157 ) {
5158 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
5159 }
5160
5161 if ( ! empty( $api_managed_permissions ) ) {
5162 $has_site_delegated_connection = false;
5163
5164 if (
5165 ! $is_enabled &&
5166 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
5167 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
5168 ) {
5169 /**
5170 * 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.
5171 *
5172 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
5173 */
5174 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
5175 }
5176
5177 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
5178 $result = $this->update_network_permissions(
5179 $api_managed_permissions,
5180 $is_enabled,
5181 $has_site_delegated_connection
5182 );
5183 } else {
5184 $result = $this->update_site_permissions(
5185 $api_managed_permissions,
5186 $is_enabled,
5187 $blog_id
5188 );
5189 }
5190
5191 if ( true !== $result ) {
5192 return $result;
5193 }
5194
5195 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
5196 if ( $is_enabled ) {
5197 $this->schedule_sync_cron();
5198 } else {
5199 $this->clear_sync_cron( ! $has_site_delegated_connection );
5200 }
5201 }
5202
5203 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
5204 $this->toggle_user_permission( $is_enabled, $blog_id );
5205 }
5206 }
5207
5208 $this->update_tracking_permissions(
5209 $permissions,
5210 $is_enabled,
5211 $blog_id
5212 );
5213
5214 return true;
5215 }
5216
5217 /**
5218 * @param bool $is_enabled
5219 * @param int|null $blog_id
5220 */
5221 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5222 $network_or_blog_ids = is_numeric( $blog_id ) ?
5223 $blog_id :
5224 fs_is_network_admin();
5225
5226 if ( $is_enabled ) {
5227 $this->reset_anonymous_mode( $network_or_blog_ids );
5228 } else {
5229 $this->skip_connection( $network_or_blog_ids );
5230 }
5231 }
5232
5233 /**
5234 * Opt-in back into usage tracking.
5235 *
5236 * Note: This will only work if the user opted-in previously.
5237 *
5238 * Returns:
5239 * 1. FALSE - If the user never opted-in.
5240 * 2. TRUE - If successfully opted-in back to usage tracking.
5241 * 3. object - API result on failure.
5242 *
5243 * @author Leo Fajardo (@leorw)
5244 * @since 1.2.1.5
5245 *
5246 * @bool $is_enabled
5247 *
5248 * @return bool|object
5249 */
5250 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5251 $this->_logger->entrance();
5252
5253 return $this->toggle_permission_tracking(
5254 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5255 $is_enabled,
5256 $blog_id
5257 );
5258 }
5259
5260 /**
5261 * If user opted-in and later disabled usage-tracking,
5262 * re-allow tracking for licensing and updates.
5263 *
5264 * @author Leo Fajardo (@leorw)
5265 * @since 1.2.1.5
5266 *
5267 * @param bool $is_context_single_site
5268 */
5269 private function reconnect_locally( $is_context_single_site = false ) {
5270 $this->_logger->entrance();
5271
5272 if ( ! $this->is_registered() ) {
5273 return;
5274 }
5275
5276 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5277 if ( $this->is_tracking_prohibited() ) {
5278 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5279 }
5280 } else {
5281 $installs_map = $this->get_blog_install_map();
5282 foreach ( $installs_map as $blog_id => $install ) {
5283 /**
5284 * @var FS_Site $install
5285 */
5286 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5287 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5288 }
5289 }
5290 }
5291 }
5292
5293 /**
5294 * 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.
5295 *
5296 * @param string[] $permissions
5297 * @param bool $is_enabled
5298 * @param int|null $blog_id
5299 *
5300 * @return array
5301 */
5302 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5303 // Alias.
5304 $permission_manager = FS_Permission_Manager::instance( $this );
5305
5306 $network_or_blog_ids = is_numeric( $blog_id ) ?
5307 $blog_id :
5308 fs_is_network_admin();
5309
5310 if ( true === $network_or_blog_ids ) {
5311 // Update the permission for all non-delegated sub-sites.
5312 $blog_ids = $this->get_non_delegated_blog_ids();
5313
5314 // Add the network-level to the array, to update the permission on the network-level storage.
5315 array_unshift( $blog_ids, null );
5316 }
5317 else
5318 {
5319 if ( false === $network_or_blog_ids ) {
5320 $network_or_blog_ids = null;
5321 }
5322
5323 $blog_ids = is_array( $network_or_blog_ids ) ?
5324 $network_or_blog_ids :
5325 array( $network_or_blog_ids );
5326 }
5327
5328 $result = array();
5329 foreach ( $permissions as $permission ) {
5330 $permission = trim( $permission );
5331 $is_permission_supported = true;
5332
5333 foreach ( $blog_ids as $id ) {
5334 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5335 $permission,
5336 $is_enabled,
5337 $id
5338 );
5339 }
5340
5341 if ( ! $is_permission_supported ) {
5342 $permission = 'no_match';
5343 }
5344
5345 $result[ $permission ] = $is_enabled;
5346 }
5347
5348 return $result;
5349 }
5350
5351 /**
5352 * Parse plugin's settings (as defined by the plugin dev).
5353 *
5354 * @author Vova Feldman (@svovaf)
5355 * @since 1.1.7.3
5356 *
5357 * @param array $plugin_info
5358 *
5359 * @throws \Freemius_Exception
5360 */
5361 private function parse_settings( &$plugin_info ) {
5362 $this->_logger->entrance();
5363
5364 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5365 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5366 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5367 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5368 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5369
5370 /**
5371 * @author Vova Feldman (@svovaf)
5372 * @since 1.1.9 Try to pull secret key from external config.
5373 */
5374 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5375 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5376 }
5377
5378 if ( isset( $plugin_info['parent'] ) ) {
5379 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5380 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5381 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5382 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5383 }
5384
5385 if ( false === $id ) {
5386 throw new Freemius_Exception( array(
5387 'error' => array(
5388 'type' => 'ParameterNotSet',
5389 'message' => 'Plugin id parameter is not set.',
5390 'code' => 'plugin_id_not_set',
5391 'http' => 500,
5392 )
5393 ) );
5394 }
5395 if ( false === $public_key ) {
5396 throw new Freemius_Exception( array(
5397 'error' => array(
5398 'type' => 'ParameterNotSet',
5399 'message' => 'Plugin public_key parameter is not set.',
5400 'code' => 'plugin_public_key_not_set',
5401 'http' => 500,
5402 )
5403 ) );
5404 }
5405
5406 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5407 $this->_plugin :
5408 new FS_Plugin();
5409
5410 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5411
5412 $plugin->update( array(
5413 'id' => $id,
5414 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5415 'public_key' => $public_key,
5416 'slug' => $this->_slug,
5417 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5418 'parent_plugin_id' => $parent_id,
5419 'version' => $this->get_plugin_version(),
5420 'title' => $this->get_plugin_name( $premium_suffix ),
5421 'file' => $this->_plugin_basename,
5422 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5423 'premium_suffix' => $premium_suffix,
5424 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5425 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5426 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5427 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5428 'opt_in_moderation' => $this->get_option( $plugin_info, 'opt_in', null ),
5429 ) );
5430
5431 if ( $plugin->is_updated() ) {
5432 // Update plugin details.
5433 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5434 }
5435 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5436 $this->_plugin->secret_key = $secret_key;
5437
5438 /**
5439 * 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).
5440 *
5441 * @author Vova Feldman
5442 * @since 2.4.5
5443 */
5444 if ( $this->is_network_active() && fs_is_network_admin() ) {
5445 if ( isset( $plugin_info['menu_network'] ) &&
5446 is_array( $plugin_info['menu_network'] ) &&
5447 ! empty( $plugin_info['menu_network'] )
5448 ) {
5449 $plugin_info['menu'] = $plugin_info['menu_network'];
5450 }
5451 }
5452
5453 if ( ! isset( $plugin_info['menu'] ) ) {
5454 $plugin_info['menu'] = array();
5455
5456 if ( ! empty( $this->_storage->sdk_last_version ) &&
5457 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5458 ) {
5459 // Backward compatibility to 1.1.2
5460 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5461 $plugin_info['menu_slug'] :
5462 $this->_slug;
5463 }
5464 }
5465
5466 $this->_menu = FS_Admin_Menu_Manager::instance(
5467 $this->_module_id,
5468 $this->_module_type,
5469 $this->get_unique_affix()
5470 );
5471
5472 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5473
5474 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5475 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5476 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5477 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5478 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5479 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5480 if ( $this->_is_premium_only ) {
5481 // If premium only plugin, disable anonymous mode.
5482 $this->_enable_anonymous = false;
5483 $this->_anonymous_mode = false;
5484 } else {
5485 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5486 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5487 }
5488 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5489 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5490
5491 if ( ! empty( $plugin_info['trial'] ) ) {
5492 $this->_trial_days = $this->get_numeric_option(
5493 $plugin_info['trial'],
5494 'days',
5495 // Default to 0 - trial without days specification.
5496 0
5497 );
5498
5499 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5500 }
5501
5502 $this->_navigation = $this->get_option(
5503 $plugin_info,
5504 'navigation',
5505 $this->is_free_wp_org_theme() ?
5506 self::NAVIGATION_TABS :
5507 self::NAVIGATION_MENU
5508 );
5509 }
5510
5511 /**
5512 * @param string[] $options
5513 * @param string $key
5514 * @param mixed $default
5515 *
5516 * @return bool
5517 */
5518 private function get_option( &$options, $key, $default = false ) {
5519 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5520 }
5521
5522 private function get_bool_option( &$options, $key, $default = false ) {
5523 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5524 }
5525
5526 private function get_numeric_option( &$options, $key, $default = false ) {
5527 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5528 }
5529
5530 /**
5531 * Gate keeper.
5532 *
5533 * @author Vova Feldman (@svovaf)
5534 * @since 1.1.7.3
5535 *
5536 * @return bool
5537 */
5538 private function should_stop_execution() {
5539 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5540 /**
5541 * Don't execute Freemius until plugin was fully loaded at least once,
5542 * to give the opportunity for the activation hook to run before pinging
5543 * the API for connectivity test. This logic is relevant for the
5544 * identification of new plugin install vs. plugin update.
5545 *
5546 * @author Vova Feldman (@svovaf)
5547 * @since 1.1.9
5548 */
5549 return true;
5550 }
5551
5552 if ( $this->is_activation_mode() ) {
5553 if ( ! is_admin() ) {
5554 /**
5555 * If in activation mode, don't execute Freemius outside the admin dashboard.
5556 *
5557 * @author Vova Feldman (@svovaf)
5558 * @since 1.1.7.3
5559 */
5560 return true;
5561 }
5562
5563 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5564 /**
5565 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5566 * then don't start Freemius.
5567 *
5568 * @author Vova Feldman (@svovaf)
5569 * @since 1.1.6.3
5570 *
5571 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5572 */
5573 return true;
5574 }
5575
5576 if ( self::is_cron() ) {
5577 /**
5578 * If in activation mode, don't execute Freemius during wp crons
5579 * (wp crons have HTTP context - called as HTTP request).
5580 *
5581 * @author Vova Feldman (@svovaf)
5582 * @since 1.1.7.3
5583 */
5584 return true;
5585 }
5586
5587 if ( self::is_ajax() ) {
5588 /**
5589 * During activation, if running in AJAX mode, unless there's a sticky
5590 * connectivity issue notice, don't run Freemius.
5591 *
5592 * @author Vova Feldman (@svovaf)
5593 * @since 1.1.7.3
5594 */
5595 return true;
5596 }
5597 }
5598
5599 return false;
5600 }
5601
5602 /**
5603 * Triggered after code type has changed.
5604 *
5605 * @author Vova Feldman (@svovaf)
5606 * @since 1.1.9.1
5607 */
5608 function _after_code_type_change() {
5609 $this->_logger->entrance();
5610
5611 if ( $this->is_theme() ) {
5612 // Expire the cache of the previous tabs since the theme may
5613 // have setting updates after code type has changed.
5614 $this->_cache->expire( 'tabs' );
5615 $this->_cache->expire( 'tabs_stylesheets' );
5616 }
5617
5618 if ( $this->is_registered() ) {
5619 if ( ! $this->is_addon() ) {
5620 add_action(
5621 is_admin() ? 'admin_init' : 'init',
5622 array( &$this, '_plugin_code_type_changed' )
5623 );
5624 }
5625
5626 if ( $this->is_premium() ) {
5627 // Purge cached payments after switching to the premium version.
5628 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5629 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5630 }
5631 }
5632 }
5633
5634 /**
5635 * Handles plugin's code type change (free <--> premium).
5636 *
5637 * @author Vova Feldman (@svovaf)
5638 * @since 1.0.9
5639 */
5640 function _plugin_code_type_changed() {
5641 $this->_logger->entrance();
5642
5643 if ( $this->is_premium() ) {
5644 $this->reconnect_locally();
5645
5646 // Activated premium code.
5647 $this->do_action( 'after_premium_version_activation' );
5648
5649 // Remove all sticky messages related to download of the premium version.
5650 $this->_admin_notices->remove_sticky( array(
5651 'trial_started',
5652 'plan_upgraded',
5653 'plan_changed',
5654 'license_activated',
5655 ) );
5656
5657 $notice = '';
5658 if ( ! $this->is_only_premium() ) {
5659 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5660 }
5661
5662 $license_notice = $this->get_license_network_activation_notice();
5663 if ( ! empty( $license_notice ) ) {
5664 $notice .= ' ' . $license_notice;
5665 }
5666
5667 if ( ! empty( $notice ) ) {
5668 $this->_admin_notices->add_sticky(
5669 trim( $notice ),
5670 'premium_activated',
5671 $this->get_text_x_inline( 'W00t',
5672 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5673 );
5674 }
5675 } else {
5676 // Remove sticky message related to premium code activation.
5677 $this->_admin_notices->remove_sticky( 'premium_activated' );
5678
5679 // Activated free code (after had the premium before).
5680 $this->do_action( 'after_free_version_reactivation' );
5681
5682 if ( $this->is_paying() && ! $this->is_premium() ) {
5683 $this->add_complete_upgrade_instructions_notice(
5684 sprintf(
5685 /* translators: %s: License type (e.g. you have a professional license) */
5686 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5687 $this->get_plan_title()
5688 ),
5689 'plan_upgraded'
5690 );
5691 }
5692 }
5693
5694 // Schedule code type changes event.
5695 $this->schedule_install_sync();
5696
5697 /**
5698 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5699 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5700 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5701 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5702 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5703 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5704 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5705 */
5706 $this->unregister_uninstall_hook();
5707
5708 $this->clear_module_main_file_cache();
5709
5710 // Update is_premium of latest version.
5711 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5712 }
5713
5714 #endregion
5715
5716 #----------------------------------------------------------------------------------
5717 #region Add-ons
5718 #----------------------------------------------------------------------------------
5719
5720 /**
5721 * Check if add-on installed and activated on site.
5722 *
5723 * @author Vova Feldman (@svovaf)
5724 * @since 1.0.6
5725 *
5726 * @param string|number $id_or_slug
5727 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5728 *
5729 * @return bool
5730 */
5731 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5732 $this->_logger->entrance();
5733
5734 $addon_id = self::get_module_id( $id_or_slug );
5735 $is_activated = self::has_instance( $addon_id );
5736
5737 if ( ! $is_activated ) {
5738 return false;
5739 }
5740
5741 if ( is_bool( $is_premium ) ) {
5742 // Check if the specified code version is activate.
5743 $addon = $this->get_addon_instance( $addon_id );
5744 $is_activated = ( $is_premium === $addon->is_premium() );
5745 }
5746
5747 return $is_activated;
5748 }
5749
5750 /**
5751 * Check if add-on was connected to install
5752 *
5753 * @author Vova Feldman (@svovaf)
5754 * @since 1.1.7
5755 *
5756 * @param string|number $id_or_slug
5757 *
5758 * @return bool
5759 */
5760 function is_addon_connected( $id_or_slug ) {
5761 $this->_logger->entrance();
5762
5763 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5764
5765 $addon_id = self::get_module_id( $id_or_slug );
5766 $addon = $this->get_addon( $addon_id );
5767 $slug = $addon->slug;
5768 if ( ! isset( $sites[ $slug ] ) ) {
5769 return false;
5770 }
5771
5772 $site = $sites[ $slug ];
5773
5774 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5775
5776 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5777 // The given slug do NOT belong to any of the plugin's add-ons.
5778 return false;
5779 }
5780
5781 return ( is_object( $site ) &&
5782 is_numeric( $site->id ) &&
5783 is_numeric( $site->user_id ) &&
5784 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5785 );
5786 }
5787
5788 /**
5789 * Determines if add-on installed.
5790 *
5791 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5792 *
5793 * @author Vova Feldman (@svovaf)
5794 * @since 1.0.6
5795 *
5796 * @param string|number $id_or_slug
5797 *
5798 * @return bool
5799 */
5800 function is_addon_installed( $id_or_slug ) {
5801 $this->_logger->entrance();
5802
5803 $addon_id = self::get_module_id( $id_or_slug );
5804
5805 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5806 }
5807
5808 /**
5809 * Get add-on basename.
5810 *
5811 * @author Vova Feldman (@svovaf)
5812 * @since 1.0.6
5813 *
5814 * @param string|number $id_or_slug
5815 *
5816 * @return string
5817 */
5818 function get_addon_basename( $id_or_slug ) {
5819 $addon_id = self::get_module_id( $id_or_slug );
5820
5821 if ( $this->is_addon_activated( $addon_id ) ) {
5822 return self::instance( $addon_id )->get_plugin_basename();
5823 }
5824
5825 $addon = $this->get_addon( $addon_id );
5826 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5827
5828 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5829 return $premium_basename;
5830 }
5831
5832 $all_plugins = $this->get_all_plugins();
5833
5834 foreach ( $all_plugins as $basename => $data ) {
5835 if ( $addon->slug === $data['slug'] ||
5836 $addon->premium_slug === $data['slug']
5837 ) {
5838 return $basename;
5839 }
5840 }
5841
5842 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5843
5844 return $free_basename;
5845 }
5846
5847 /**
5848 * Get installed add-ons instances.
5849 *
5850 * @author Vova Feldman (@svovaf)
5851 * @since 1.0.6
5852 *
5853 * @return Freemius[]
5854 */
5855 function get_installed_addons() {
5856 if ( $this->is_addon() ) {
5857 // Add-on cannot have add-ons.
5858 return array();
5859 }
5860
5861 $installed_addons = array();
5862
5863 foreach ( self::$_instances as $instance ) {
5864 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5865 $installed_addons[] = $instance;
5866 }
5867 }
5868
5869 return $installed_addons;
5870 }
5871
5872 /**
5873 * Check if any add-ons of the plugin are installed.
5874 *
5875 * @author Leo Fajardo (@leorw)
5876 * @since 1.1.1
5877 *
5878 * @return bool
5879 */
5880 function has_installed_addons() {
5881 if ( ! $this->has_addons() ) {
5882 return false;
5883 }
5884
5885 foreach ( self::$_instances as $instance ) {
5886 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5887 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5888 return true;
5889 }
5890 }
5891 }
5892
5893 return false;
5894 }
5895
5896 /**
5897 * Tell Freemius that the current plugin is an add-on.
5898 *
5899 * @author Vova Feldman (@svovaf)
5900 * @since 1.0.6
5901 *
5902 * @param number $parent_plugin_id The parent plugin ID
5903 */
5904 function init_addon( $parent_plugin_id ) {
5905 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5906 }
5907
5908 /**
5909 * @author Vova Feldman (@svovaf)
5910 * @since 1.0.6
5911 *
5912 * @return bool
5913 */
5914 function is_addon() {
5915 return (
5916 isset( $this->_plugin->parent_plugin_id ) &&
5917 is_numeric( $this->_plugin->parent_plugin_id )
5918 );
5919 }
5920
5921 /**
5922 * @author Vova Feldman (@svovaf)
5923 * @since 2.3.2
5924 *
5925 * @param number $parent_product_id
5926 *
5927 * @return bool
5928 */
5929 function is_addon_of( $parent_product_id ) {
5930 return (
5931 $this->is_addon() &&
5932 $parent_product_id == $this->_plugin->parent_plugin_id
5933 );
5934 }
5935
5936 /**
5937 * Deactivate add-on if it's premium only and the user does't have a valid license.
5938 *
5939 * @param bool $is_after_trial_cancel
5940 *
5941 * @return bool If add-on was deactivated.
5942 */
5943 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5944 if ( ! $this->has_free_plan() &&
5945 ! $this->has_features_enabled_license() &&
5946 ! $this->_has_premium_license()
5947 ) {
5948 if ( $this->is_registered() ) {
5949 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5950 // if (empty($this->_storage->activation_timestamp) ||
5951 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5952 // ) {
5953 /**
5954 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5955 *
5956 * Retry syncing the user add-on licenses.
5957 */
5958 // Sync licenses.
5959 $this->_sync_licenses();
5960 // }
5961
5962 // Try to activate premium license.
5963 $this->_activate_license( true );
5964 }
5965
5966 if ( ! $this->has_free_plan() &&
5967 ! $this->has_features_enabled_license() &&
5968 ! $this->_has_premium_license()
5969 ) {
5970 // @todo Check if deactivate plugins also call the deactivation hook.
5971
5972 $this->_parent->_admin_notices->add_sticky(
5973 sprintf(
5974 ( $is_after_trial_cancel ?
5975 $this->_parent->get_text_inline(
5976 '%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.',
5977 'addon-trial-cancelled-message'
5978 ) :
5979 $this->_parent->get_text_inline(
5980 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5981 'addon-no-license-message'
5982 )
5983 ),
5984 '<b>' . $this->_plugin->title . '</b>'
5985 ) . ' ' . sprintf(
5986 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5987 $this->_parent->addon_url( $this->_slug ),
5988 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5989 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5990 ),
5991 'no_addon_license_' . $this->_slug,
5992 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5993 ( $is_after_trial_cancel ? 'success' : 'error' )
5994 );
5995
5996 deactivate_plugins( array( $this->_plugin_basename ), true );
5997
5998 return true;
5999 }
6000 }
6001
6002 return false;
6003 }
6004
6005 #endregion
6006
6007 #----------------------------------------------------------------------------------
6008 #region Sandbox
6009 #----------------------------------------------------------------------------------
6010
6011 /**
6012 * Set Freemius into sandbox mode for debugging.
6013 *
6014 * @author Vova Feldman (@svovaf)
6015 * @since 1.0.4
6016 *
6017 * @param string $secret_key
6018 */
6019 function init_sandbox( $secret_key ) {
6020 $this->_plugin->secret_key = $secret_key;
6021
6022 // Update plugin details.
6023 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6024 }
6025
6026 /**
6027 * Check if running payments in sandbox mode.
6028 *
6029 * @author Vova Feldman (@svovaf)
6030 * @since 1.0.4
6031 *
6032 * @return bool
6033 */
6034 function is_payments_sandbox() {
6035 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6036 }
6037
6038 #endregion
6039
6040 /**
6041 * Check if running test vs. live plugin.
6042 *
6043 * @author Vova Feldman (@svovaf)
6044 * @since 1.0.5
6045 *
6046 * @return bool
6047 */
6048 function is_live() {
6049 return $this->_plugin->is_live;
6050 }
6051
6052 /**
6053 * Check if super-admin skipped connection for all sites in the network.
6054 *
6055 * @author Vova Feldman (@svovaf)
6056 * @since 2.0.0
6057 */
6058 function is_network_anonymous() {
6059 if ( ! $this->_is_network_active ) {
6060 return false;
6061 }
6062
6063 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6064
6065 if ( empty( $is_anonymous_ms ) ) {
6066 return false;
6067 }
6068
6069 return $is_anonymous_ms['is'];
6070 }
6071
6072 /**
6073 * Check if super-admin opted-in for all sites in the network.
6074 *
6075 * @author Vova Feldman (@svovaf)
6076 * @since 2.0.0
6077 */
6078 function is_network_connected() {
6079 if ( ! $this->_is_network_active ) {
6080 return false;
6081 }
6082
6083 return $this->_storage->get( 'is_network_connected' );
6084 }
6085
6086 /**
6087 * Check if the user skipped connecting the account with Freemius.
6088 *
6089 * @author Vova Feldman (@svovaf)
6090 * @since 1.0.7
6091 *
6092 * @return bool
6093 */
6094 function is_anonymous() {
6095 if ( ! isset( $this->_is_anonymous ) ) {
6096 if ( $this->is_network_anonymous() ) {
6097 $this->_is_anonymous = true;
6098 } else if ( fs_is_network_admin() ) {
6099 /**
6100 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
6101 */
6102 $non_delegated_sites = $this->get_non_delegated_blog_ids();
6103
6104 foreach ( $non_delegated_sites as $blog_id ) {
6105 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6106
6107 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
6108 $this->_is_anonymous = false;
6109 break;
6110 }
6111 }
6112
6113 if ( false !== $this->_is_anonymous ) {
6114 $this->_is_anonymous = true;
6115 }
6116 } else {
6117 if ( ! isset( $this->_storage->is_anonymous ) ) {
6118 // Not skipped.
6119 $this->_is_anonymous = false;
6120 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6121 // For back compatibility, since the variable was boolean before.
6122 $this->_is_anonymous = $this->_storage->is_anonymous;
6123
6124 // Upgrade stored data format to 1.1.3 format.
6125 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6126 } else {
6127 // Version 1.1.3 and later.
6128 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6129 }
6130 }
6131 }
6132
6133 return $this->_is_anonymous;
6134 }
6135
6136 /**
6137 * Check if the user skipped the connection of a specified site.
6138 *
6139 * @author Vova Feldman (@svovaf)
6140 * @since 2.0.0
6141 *
6142 * @param int $blog_id
6143 *
6144 * @return bool
6145 */
6146 function is_anonymous_site( $blog_id = 0 ) {
6147 if ( $this->is_network_anonymous() ) {
6148 return true;
6149 }
6150
6151 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6152
6153 if ( empty( $is_anonymous ) ) {
6154 return false;
6155 }
6156
6157 return $is_anonymous['is'];
6158 }
6159
6160 /**
6161 * Check if user connected his account and install pending email activation.
6162 *
6163 * @author Vova Feldman (@svovaf)
6164 * @since 1.0.7
6165 *
6166 * @return bool
6167 */
6168 function is_pending_activation() {
6169 return $this->_storage->get( 'is_pending_activation', false );
6170 }
6171
6172 /**
6173 * @author Leo Fajardo (@leorw)
6174 * @since 2.5.0
6175 */
6176 private function clear_pending_activation_mode() {
6177 // Remove the pending activation sticky notice (if it still exists).
6178 $this->_admin_notices->remove_sticky( 'activation_pending' );
6179
6180 // Clear the plugin's pending activation mode.
6181 unset( $this->_storage->is_pending_activation );
6182 }
6183
6184 /**
6185 * Check if plugin must be WordPress.org compliant.
6186 *
6187 * @since 1.0.7
6188 *
6189 * @return bool
6190 */
6191 function is_org_repo_compliant() {
6192 return $this->_is_org_compliant;
6193 }
6194
6195 #--------------------------------------------------------------------------------
6196 #region WP Cron Common
6197 #--------------------------------------------------------------------------------
6198
6199 /**
6200 * @author Vova Feldman (@svovaf)
6201 * @since 2.0.0
6202 *
6203 * @param string $name Cron name.
6204 *
6205 * @return object
6206 */
6207 private function get_cron_data( $name ) {
6208 $this->_logger->entrance( $name );
6209
6210 /**
6211 * @var object $cron_data
6212 */
6213 return $this->_storage->get( "{$name}_cron", null );
6214 }
6215
6216 /**
6217 * @author Vova Feldman (@svovaf)
6218 * @since 2.0.0
6219 *
6220 * @param string $name Cron name.
6221 */
6222 private function clear_cron_data( $name ) {
6223 $this->_logger->entrance( $name );
6224
6225 $this->_storage->remove( "{$name}_cron" );
6226 }
6227
6228 /**
6229 * @author Vova Feldman (@svovaf)
6230 * @since 2.0.0
6231 *
6232 * @param string $name Cron name.
6233 * @param int $cron_blog_id The cron executing blog ID.
6234 */
6235 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6236 $this->_logger->entrance( $name );
6237
6238 $this->_storage->store( "{$name}_cron", (object) array(
6239 'version' => $this->get_plugin_version(),
6240 'blog_id' => $cron_blog_id,
6241 'sdk_version' => $this->version,
6242 'timestamp' => WP_FS__SCRIPT_START_TIME,
6243 'on' => true,
6244 ) );
6245 }
6246
6247 /**
6248 * Get the cron's executing blog ID.
6249 *
6250 * @author Vova Feldman (@svovaf)
6251 * @since 2.0.0
6252 *
6253 * @param string $name Cron name.
6254 *
6255 * @return int
6256 */
6257 private function get_cron_blog_id( $name ) {
6258 $this->_logger->entrance( $name );
6259
6260 if ( ! is_multisite() ) {
6261 // Not a multisite.
6262 return 0;
6263 }
6264
6265 $cron_data = $this->get_cron_data( $name );
6266
6267 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6268 $cron_data->blog_id :
6269 0;
6270 }
6271
6272 /**
6273 * @author Vova Feldman (@svovaf)
6274 * @since 2.0.0
6275 *
6276 * @param string $name Cron name.
6277 *
6278 * @return bool
6279 */
6280 private function is_cron_on( $name ) {
6281 $this->_logger->entrance( $name );
6282
6283 /**
6284 * @var object $cron_data
6285 */
6286 $cron_data = $this->get_cron_data( $name );
6287
6288 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6289 }
6290
6291 /**
6292 * Unix timestamp for previous cron execution or false if never executed.
6293 *
6294 * @author Vova Feldman (@svovaf)
6295 * @since 2.0.0
6296 *
6297 * @param string $name Cron name.
6298 *
6299 * @return int|false
6300 */
6301 private function cron_last_execution( $name ) {
6302 $this->_logger->entrance( $name );
6303
6304 return $this->_storage->get( "{$name}_timestamp" );
6305 }
6306
6307 /**
6308 * Set cron execution time to now.
6309 *
6310 * @author Vova Feldman (@svovaf)
6311 * @since 2.0.0
6312 *
6313 * @param string $name Cron name.
6314 */
6315 private function set_cron_execution_timestamp( $name ) {
6316 $this->_logger->entrance( $name );
6317
6318 $this->_storage->store( "{$name}_timestamp", time() );
6319 }
6320
6321 /**
6322 * Sets the keepalive time to now.
6323 *
6324 * @author Leo Fajardo (@leorw)
6325 * @since 2.2.3
6326 *
6327 * @param bool|null $use_network_level_storage
6328 */
6329 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6330 $this->_logger->entrance();
6331
6332 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6333 }
6334
6335 /**
6336 * Check if cron was executed in the last $period of seconds.
6337 *
6338 * @author Vova Feldman (@svovaf)
6339 * @since 2.0.0
6340 *
6341 * @param string $name Cron name.
6342 * @param int $period In seconds
6343 *
6344 * @return bool
6345 */
6346 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6347 $this->_logger->entrance( $name );
6348
6349 $last_execution = $this->cron_last_execution( $name );
6350
6351 if ( ! is_numeric( $last_execution ) ) {
6352 return false;
6353 }
6354
6355 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6356 }
6357
6358 /**
6359 * WP Cron is executed on a site level. When running in a multisite network environment
6360 * with the network integration activated, for optimization reasons, we are consolidating
6361 * the installs data sync cron to be executed only from a single site.
6362 *
6363 * @author Vova Feldman (@svovaf)
6364 * @since 2.0.0
6365 *
6366 * @param int $except_blog_id Target any except the excluded blog ID.
6367 *
6368 * @return int
6369 */
6370 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6371 if ( ! is_multisite() ) {
6372 return 0;
6373 }
6374
6375 if ( $this->_is_network_active ) {
6376 $network_install_blog_id = $this->_storage->network_install_blog_id;
6377
6378 if (
6379 is_numeric( $network_install_blog_id ) &&
6380 $except_blog_id != $network_install_blog_id &&
6381 self::is_site_active( $network_install_blog_id )
6382 ) {
6383 // Try to run cron from the main network blog.
6384 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6385
6386 if (
6387 is_object( $install ) &&
6388 $this->is_tracking_allowed( $network_install_blog_id, $install )
6389 ) {
6390 return $network_install_blog_id;
6391 }
6392 }
6393 }
6394
6395 // Get first opted-in blog ID with active tracking.
6396 $installs = $this->get_blog_install_map();
6397 foreach ( $installs as $blog_id => $install ) {
6398 if ( $except_blog_id != $blog_id &&
6399 self::is_site_active( $blog_id ) &&
6400 $this->is_tracking_allowed( $blog_id, $install )
6401 ) {
6402 return $blog_id;
6403 }
6404 }
6405
6406 return 0;
6407 }
6408
6409 /**
6410 * @author Vova Feldman (@svovaf)
6411 * @since 2.0.0
6412 *
6413 * @param string $name Cron name.
6414 * @param string $action_tag Callback action tag.
6415 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6416 */
6417 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6418 $this->_logger->entrance( $name );
6419
6420 if ( ! $this->is_cron_on( $name ) ) {
6421 return;
6422 }
6423
6424 $clear_cron = true;
6425 if ( ! $is_network_clear && $this->_is_network_active ) {
6426 $installs = $this->get_blog_install_map();
6427
6428 foreach ( $installs as $blog_id => $install ) {
6429 /**
6430 * @var FS_Site $install
6431 */
6432 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6433 $clear_cron = false;
6434 break;
6435 }
6436 }
6437 }
6438
6439 if ( ! $clear_cron ) {
6440 return;
6441 }
6442
6443 $cron_blog_id = $this->get_cron_blog_id( $name );
6444
6445 $this->clear_cron_data( $name );
6446
6447 if ( 0 < $cron_blog_id ) {
6448 switch_to_blog( $cron_blog_id );
6449 }
6450
6451 if ( empty( $action_tag ) ) {
6452 $action_tag = $name;
6453 }
6454
6455 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6456
6457 if ( 0 < $cron_blog_id ) {
6458 restore_current_blog();
6459 }
6460 }
6461
6462 /**
6463 * Unix timestamp for next cron execution or false if not scheduled.
6464 *
6465 * @author Vova Feldman (@svovaf)
6466 * @since 2.0.0
6467 *
6468 * @param string $name Cron name.
6469 * @param string $action_tag Callback action tag.
6470 *
6471 * @return int|false
6472 */
6473 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6474 $this->_logger->entrance( $name );
6475
6476 if ( ! $this->is_cron_on( $name ) ) {
6477 return false;
6478 }
6479
6480 $cron_blog_id = $this->get_cron_blog_id( $name );
6481
6482 if ( 0 < $cron_blog_id ) {
6483 switch_to_blog( $cron_blog_id );
6484 }
6485
6486 if ( empty( $action_tag ) ) {
6487 $action_tag = $name;
6488 }
6489
6490 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6491
6492 if ( 0 < $cron_blog_id ) {
6493 restore_current_blog();
6494 }
6495
6496 return $next_scheduled;
6497 }
6498
6499 /**
6500 * @author Vova Feldman (@svovaf)
6501 * @since 2.0.0
6502 *
6503 * @param string $name Cron name.
6504 * @param string $action_tag Callback action tag.
6505 * @param string $recurrence 'single' or 'daily'.
6506 * @param int $start_at Defaults to now.
6507 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6508 * @param int $except_blog_id Target any except the excluded blog ID.
6509 */
6510 private function schedule_cron(
6511 $name,
6512 $action_tag = '',
6513 $recurrence = 'single',
6514 $start_at = WP_FS__SCRIPT_START_TIME,
6515 $randomize_start = true,
6516 $except_blog_id = 0
6517 ) {
6518 $this->_logger->entrance( $name );
6519
6520 $this->clear_cron( $name, $action_tag, true );
6521
6522 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6523
6524 if ( is_multisite() && 0 == $cron_blog_id ) {
6525 // Don't schedule cron since couldn't find a target blog.
6526 return;
6527 }
6528
6529 if ( 0 < $cron_blog_id ) {
6530 switch_to_blog( $cron_blog_id );
6531 }
6532
6533 if ( 'daily' === $recurrence ) {
6534 if ( $randomize_start ) {
6535 // Schedule first sync with a random 12 hour time range from now.
6536 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6537 }
6538
6539 // Schedule daily WP cron.
6540 wp_schedule_event(
6541 $start_at,
6542 'daily',
6543 $this->get_action_tag( $action_tag )
6544 );
6545 } else if ( 'single' === $recurrence ) {
6546 // Schedule single cron.
6547 wp_schedule_single_event(
6548 $start_at,
6549 $this->get_action_tag( $action_tag )
6550 );
6551 }
6552
6553 $this->set_cron_data( $name, $cron_blog_id );
6554
6555 if ( 0 < $cron_blog_id ) {
6556 restore_current_blog();
6557 }
6558 }
6559
6560 /**
6561 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6562 * that doesn't halt page loading.
6563 *
6564 * @author Vova Feldman (@svovaf)
6565 * @since 2.0.0
6566 *
6567 * @param string $name Cron name.
6568 * @param callable $callable The function that should be executed.
6569 */
6570 private function execute_cron( $name, $callable ) {
6571 $this->_logger->entrance( $name );
6572
6573 // Store the last time data sync was executed.
6574 $this->set_cron_execution_timestamp( $name );
6575
6576 // Check if API is temporary down.
6577 if ( FS_Api::is_temporary_down() ) {
6578 return;
6579 }
6580
6581 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6582
6583 $users_2_blog_ids = array();
6584
6585 if ( ! is_multisite() ) {
6586 // Add dummy blog.
6587 $users_2_blog_ids[0] = array( 0 );
6588 } else {
6589 $installs = $this->get_blog_install_map();
6590 foreach ( $installs as $blog_id => $install ) {
6591 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6592 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6593 $users_2_blog_ids[ $install->user_id ] = array();
6594 }
6595
6596 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6597 }
6598 }
6599 }
6600
6601 $current_blog_id = get_current_blog_id();
6602
6603 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6604 if ( 0 < $blog_ids[0] ) {
6605 $this->switch_to_blog( $blog_ids[0] );
6606 }
6607
6608 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6609
6610 foreach ( $blog_ids as $blog_id ) {
6611 $this->do_action( "after_{$name}_cron", $blog_id );
6612 }
6613 }
6614
6615 if ( is_multisite() ) {
6616 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6617
6618 $this->do_action( "after_{$name}_cron_multisite" );
6619 }
6620 }
6621
6622 #endregion
6623
6624 #----------------------------------------------------------------------------------
6625 #region Daily Sync Cron
6626 #----------------------------------------------------------------------------------
6627
6628
6629 /**
6630 * @author Vova Feldman (@svovaf)
6631 * @since 2.0.0
6632 *
6633 * @return bool
6634 */
6635 private function is_sync_cron_scheduled() {
6636 return $this->is_cron_on( 'sync' );
6637 }
6638
6639 /**
6640 * Get the sync cron's executing blog ID.
6641 *
6642 * @author Vova Feldman (@svovaf)
6643 * @since 2.0.0
6644 *
6645 * @return int
6646 */
6647 private function get_sync_cron_blog_id() {
6648 return $this->get_cron_blog_id( 'sync' );
6649 }
6650
6651 /**
6652 * @author Vova Feldman (@svovaf)
6653 * @since 1.1.7.3
6654 */
6655 private function run_manual_sync() {
6656 if ( ! $this->is_user_admin() ) {
6657 return;
6658 }
6659
6660 // Run manual sync.
6661 $this->_sync_cron();
6662
6663 // Reschedule next cron to run 24 hours from now (performance optimization).
6664 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6665 }
6666
6667 /**
6668 * Data sync cron job. Replaces the background sync non blocking HTTP request
6669 * that doesn't halt page loading.
6670 *
6671 * @author Vova Feldman (@svovaf)
6672 * @since 1.1.7.3
6673 * @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.
6674 */
6675 function _sync_cron() {
6676 $this->_logger->entrance();
6677
6678 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6679 }
6680
6681 /**
6682 * The actual data sync cron logic.
6683 *
6684 * @author Vova Feldman (@svovaf)
6685 * @since 2.0.0
6686 *
6687 * @param int[] $blog_ids
6688 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6689 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6690 * updates for a single site in case `execute_cron` has switched to a different blog.
6691 */
6692 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6693 if ( $this->is_registered() ) {
6694 if ( $this->has_paid_plan() ) {
6695 // Initiate background plan sync.
6696 $this->_sync_license( true, false, $current_blog_id );
6697
6698 if ( $this->is_paying() ) {
6699 // Check for premium plugin updates.
6700 $this->check_updates( true );
6701 }
6702 } else {
6703 // Sync install(s) (only if something changed locally).
6704 if ( 1 < count( $blog_ids ) ) {
6705 $this->sync_installs();
6706 } else {
6707 $this->sync_install();
6708 }
6709
6710 $this->maybe_sync_install_user();
6711 }
6712 }
6713 }
6714
6715 /**
6716 * Check if sync was executed in the last $period of seconds.
6717 *
6718 * @author Vova Feldman (@svovaf)
6719 * @since 1.1.7.3
6720 *
6721 * @param int $period In seconds
6722 *
6723 * @return bool
6724 */
6725 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6726 return $this->is_cron_executed( 'sync', $period );
6727 }
6728
6729 /**
6730 * @author Vova Feldman (@svovaf)
6731 * @since 1.1.7.3
6732 *
6733 * @return bool
6734 */
6735 private function is_sync_cron_on() {
6736 return $this->is_cron_on( 'sync' );
6737 }
6738
6739 /**
6740 * @author Leo Fajardo (@leorw)
6741 * @since 2.5.0
6742 */
6743 private function maybe_schedule_sync_cron() {
6744 $next_schedule = $this->next_sync_cron();
6745
6746 // The event is properly scheduled, so no need to reschedule it.
6747 if (
6748 is_numeric( $next_schedule ) &&
6749 $next_schedule > time()
6750 ) {
6751 return;
6752 }
6753
6754 $this->schedule_sync_cron();
6755 }
6756
6757 /**
6758 * @author Vova Feldman (@svovaf)
6759 * @since 1.1.7.3
6760 *
6761 * @param int $start_at Defaults to now.
6762 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6763 * @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.
6764 */
6765 private function schedule_sync_cron(
6766 $start_at = WP_FS__SCRIPT_START_TIME,
6767 $randomize_start = true,
6768 $except_blog_id = 0
6769 ) {
6770 $this->schedule_cron(
6771 'sync',
6772 'data_sync',
6773 'daily',
6774 $start_at,
6775 $randomize_start,
6776 $except_blog_id
6777 );
6778 }
6779
6780 /**
6781 * Add the actual sync function to the cron job hook.
6782 *
6783 * @author Vova Feldman (@svovaf)
6784 * @since 1.1.7.3
6785 */
6786 private function hook_callback_to_sync_cron() {
6787 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6788 }
6789
6790 /**
6791 * @author Vova Feldman (@svovaf)
6792 * @since 1.1.7.3
6793 *
6794 * @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.
6795 */
6796 private function clear_sync_cron( $is_network_clear = false ) {
6797 $this->_logger->entrance();
6798
6799 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6800 }
6801
6802 /**
6803 * Unix timestamp for next sync cron execution or false if not scheduled.
6804 *
6805 * @author Vova Feldman (@svovaf)
6806 * @since 1.1.7.3
6807 *
6808 * @return int|false
6809 */
6810 function next_sync_cron() {
6811 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6812 }
6813
6814 /**
6815 * Unix timestamp for previous sync cron execution or false if never executed.
6816 *
6817 * @author Vova Feldman (@svovaf)
6818 * @since 1.1.7.3
6819 *
6820 * @return int|false
6821 */
6822 function last_sync_cron() {
6823 return $this->cron_last_execution( 'sync' );
6824 }
6825
6826 #endregion Daily Sync Cron ------------------------------------------------------------------
6827
6828 #----------------------------------------------------------------------------------
6829 #region Async Install Sync
6830 #----------------------------------------------------------------------------------
6831
6832 /**
6833 * @author Vova Feldman (@svovaf)
6834 * @since 1.1.7.3
6835 *
6836 * @return bool
6837 */
6838 private function is_install_sync_scheduled() {
6839 return $this->is_cron_on( 'install_sync' );
6840 }
6841
6842 /**
6843 * Get the sync cron's executing blog ID.
6844 *
6845 * @author Vova Feldman (@svovaf)
6846 * @since 2.0.0
6847 *
6848 * @return int
6849 */
6850 private function get_install_sync_cron_blog_id() {
6851 return $this->get_cron_blog_id( 'install_sync' );
6852 }
6853
6854 /**
6855 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6856 *
6857 * @author Vova Feldman (@svovaf)
6858 * @since 1.1.7.3
6859 *
6860 * @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.
6861 */
6862 private function schedule_install_sync( $except_blog_id = 0 ) {
6863 if ( $this->is_clone() ) {
6864 return;
6865 }
6866
6867 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6868 }
6869
6870 /**
6871 * Unix timestamp for previous install sync cron execution or false if never executed.
6872 *
6873 * @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.
6874 *
6875 * @author Vova Feldman (@svovaf)
6876 * @since 1.1.7.3
6877 *
6878 * @return int|false
6879 */
6880 function last_install_sync() {
6881 return $this->cron_last_execution( 'install_sync' );
6882 }
6883
6884 /**
6885 * Unix timestamp for next install sync cron execution or false if not scheduled.
6886 *
6887 * @author Vova Feldman (@svovaf)
6888 * @since 1.1.7.3
6889 *
6890 * @return int|false
6891 */
6892 function next_install_sync() {
6893 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6894 }
6895
6896 /**
6897 * Add the actual install sync function to the cron job hook.
6898 *
6899 * @author Vova Feldman (@svovaf)
6900 * @since 1.1.7.3
6901 */
6902 private function hook_callback_to_install_sync() {
6903 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6904 }
6905
6906 /**
6907 * @author Vova Feldman (@svovaf)
6908 * @since 1.1.7.3
6909 *
6910 * @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.
6911 */
6912 private function clear_install_sync_cron( $is_network_clear = false ) {
6913 $this->_logger->entrance();
6914
6915 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6916 }
6917
6918 /**
6919 * @author Vova Feldman (@svovaf)
6920 * @since 1.1.7.3
6921 * @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.
6922 */
6923 public function _run_sync_install() {
6924 $this->_logger->entrance();
6925
6926 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6927 }
6928
6929 /**
6930 * The actual install(s) sync cron logic.
6931 *
6932 * @author Vova Feldman (@svovaf)
6933 * @since 2.0.0
6934 *
6935 * @param int[] $blog_ids
6936 * @param int|null $current_blog_id
6937 */
6938 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6939 if ( $this->is_registered() ) {
6940 if ( 1 < count( $blog_ids ) ) {
6941 $this->sync_installs( array(), true );
6942 } else {
6943 $this->sync_install( array(), true );
6944 }
6945
6946 $this->maybe_sync_install_user();
6947 }
6948 }
6949
6950 #endregion Async Install Sync ------------------------------------------------------------------
6951
6952 /**
6953 * Show a notice that activation is currently pending.
6954 *
6955 * @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.
6956 *
6957 * @author Vova Feldman (@svovaf)
6958 * @since 1.0.7
6959 *
6960 * @param bool|string $email_address
6961 * @param bool $is_pending_trial Since 1.2.1.5
6962 * @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.
6963 * @param bool $has_upgrade_context Since 2.5.3
6964 * @param bool $support_email_address Since 2.5.3
6965 */
6966 function _add_pending_activation_notice(
6967 $email_address = false,
6968 $is_pending_trial = false,
6969 $is_suspicious_email = false,
6970 $has_upgrade_context = false,
6971 $support_email_address = false
6972 ) {
6973 if ( ! is_string( $email_address ) ) {
6974 $current_user = self::_get_current_wp_user();
6975 $email_address = $current_user->user_email;
6976 }
6977
6978 $formatted_message_args = array(
6979 "<b>{$this->get_plugin_name()}</b>",
6980 "<b>{$email_address}</b>",
6981 );
6982
6983 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
6984 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
6985 $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' );
6986
6987 $formatted_message_args[] = $is_pending_trial ?
6988 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6989 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
6990
6991 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
6992 } else {
6993 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
6994 $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.' );
6995
6996 if ( $this->has_release_on_freemius() ) {
6997 $formatted_message_args[] = $this->get_text_x_inline(
6998 'the installation instructions',
6999 'Part of the message telling the user what they should receive via email.',
7000 'the-installation-instructions-phrase'
7001 );
7002 } else {
7003 $formatted_message_args[] = $this->get_text_x_inline(
7004 'a license key',
7005 'Part of the message telling the user what they should receive via email.',
7006 'a-license-key-phrase'
7007 );
7008
7009 $formatted_message .= ( ' ' . sprintf(
7010 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
7011 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
7012 sprintf(
7013 '<b><a href="%s">%s</a></b>',
7014 $this->get_activation_url( array(
7015 'fs_action' => 'reset_pending_activation_mode',
7016 'require_license' => 'true',
7017 'fs_unique_affix' => $this->get_unique_affix(),
7018 ) ),
7019 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
7020 )
7021 ) );
7022 }
7023
7024 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
7025 ( "<b>{$support_email_address}</b>" ) :
7026 $this->get_text_x_inline(
7027 "the product's support email address",
7028 'Part of the message that tells the user to check their spam folder for a specific email.',
7029 'product-support-email-address-phrase'
7030 );
7031
7032 $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' ) );
7033
7034 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
7035 }
7036
7037 $this->_admin_notices->add_sticky(
7038 vsprintf( $formatted_message, $formatted_message_args ),
7039 'activation_pending',
7040 $notice_title
7041 );
7042 }
7043
7044 /**
7045 * Check if currently in plugin activation.
7046 *
7047 * @author Vova Feldman (@svovaf)
7048 * @since 1.1.4
7049 *
7050 * @return bool
7051 */
7052 function is_plugin_activation() {
7053 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7054
7055 return !empty($result);
7056 }
7057
7058 /**
7059 *
7060 * NOTE: admin_menu action executed before admin_init.
7061 *
7062 * @author Vova Feldman (@svovaf)
7063 * @since 1.0.7
7064 */
7065 function _admin_init_action() {
7066 $is_migration = $this->is_migration();
7067
7068 /**
7069 * Automatically redirect to connect/activation page after plugin activation.
7070 *
7071 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7072 */
7073 if ( $this->is_plugin_activation() ) {
7074 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7075
7076 if ( isset( $_GET['activate-multi'] ) ) {
7077 /**
7078 * Don't redirect if activating multiple plugins at once (bulk activation).
7079 */
7080 } else if (
7081 self::is_deactivation_snoozed() &&
7082 (
7083 // Either running the free code base.
7084 ! $this->is_premium() ||
7085 // Or if has a free version.
7086 ! $this->is_only_premium() ||
7087 // If premium only, don't redirect if license is activated.
7088 ( $this->is_registered() && ! $this->can_use_premium_code() )
7089 )
7090 ) {
7091 /**
7092 * 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.
7093 */
7094 } else if ( ! $is_migration ) {
7095 $this->_redirect_on_activation_hook();
7096 return;
7097 }
7098 }
7099
7100 if ( $is_migration ) {
7101 return;
7102 }
7103
7104 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7105 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7106
7107 $this->skip_connection( fs_is_network_admin() );
7108
7109 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7110 }
7111
7112 if ( $this->is_network_activation_mode() &&
7113 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7114 ) {
7115 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7116
7117 $this->delegate_connection();
7118
7119 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7120 }
7121
7122 $this->_add_upgrade_action_link();
7123
7124 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7125 (
7126 ( true === $this->_storage->require_license_activation ) ||
7127 // Not registered nor anonymous.
7128 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7129 // OR, network level and in network upgrade mode.
7130 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7131 )
7132 ) {
7133 if ( ! $this->is_pending_activation() ) {
7134 if ( ! $this->is_activation_page() ) {
7135 /**
7136 * If a user visits any other admin page before activating the premium-only theme with a valid
7137 * license, reactivate the previous theme.
7138 *
7139 * @author Leo Fajardo (@leorw)
7140 * @since 1.2.2
7141 */
7142 if ( $this->is_theme() &&
7143 ! $this->has_settings_menu() &&
7144 ! isset( $_REQUEST['fs_action'] ) &&
7145 $this->can_activate_previous_theme()
7146 ) {
7147 if ( $this->is_only_premium() ) {
7148 $this->activate_previous_theme();
7149 return;
7150 }
7151
7152 if ( true === $this->_storage->require_license_activation ) {
7153 $this->_storage->require_license_activation = false;
7154 }
7155 }
7156
7157 if ( ! fs_is_network_admin() &&
7158 $this->is_network_activation_mode() &&
7159 ! $this->is_delegated_connection()
7160 ) {
7161 return;
7162 }
7163
7164 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7165 if ( ! $this->_anonymous_mode &&
7166 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7167 // Show notice for new plugin installations.
7168 $this->_admin_notices->add(
7169 sprintf(
7170 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7171 sprintf( '<b><a href="%s">%s</a></b>',
7172 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7173 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7174 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7175 )
7176 ),
7177 '',
7178 'update-nag'
7179 );
7180 }
7181 } else {
7182 if ( $this->should_add_sticky_optin_notice() ) {
7183 $this->add_sticky_optin_admin_notice();
7184 }
7185
7186 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7187 // Don't show admin nag if plugin update.
7188 wp_enqueue_script( 'wp-pointer' );
7189 wp_enqueue_style( 'wp-pointer' );
7190
7191 $this->_enqueue_connect_essentials();
7192
7193 add_action( 'admin_print_footer_scripts', array(
7194 $this,
7195 '_add_connect_pointer_script'
7196 ) );
7197 }
7198 }
7199 }
7200 }
7201
7202 if ( $this->show_opt_in_on_themes_page() &&
7203 $this->is_activation_page()
7204 ) {
7205 $this->_show_theme_activation_optin_dialog();
7206 }
7207 }
7208 }
7209
7210 /**
7211 * @author Vova Feldman (@svovaf)
7212 * @since 2.0.0
7213 *
7214 * @return bool
7215 */
7216 private function should_add_sticky_optin_notice() {
7217 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7218 return false;
7219 }
7220
7221 if ( fs_is_network_admin() ) {
7222 if ( ! $this->_is_network_active ) {
7223 return false;
7224 }
7225
7226 if ( ! $this->is_network_activation_mode() ) {
7227 return false;
7228 }
7229
7230 return ! isset( $this->_storage->sticky_optin_added_ms );
7231 }
7232
7233 if ( ! $this->is_activation_mode() ) {
7234 return false;
7235 }
7236
7237 // If running from a blog admin and delegated the connection.
7238 return ! isset( $this->_storage->sticky_optin_added );
7239 }
7240
7241 /**
7242 * @author Leo Fajardo (@leorw)
7243 * @since 2.0.0
7244 */
7245 private function add_sticky_optin_admin_notice() {
7246 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7247 $this->_storage->sticky_optin_added = true;
7248 } else {
7249 $this->_storage->sticky_optin_added_ms = true;
7250 }
7251
7252 // Show notice for new plugin installations.
7253 $this->_admin_notices->add_sticky(
7254 sprintf(
7255 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7256 $this->_module_type,
7257 sprintf( '<b><a href="%s">%s</a></b>',
7258 $this->get_activation_url(),
7259 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7260 )
7261 ),
7262 'connect_account',
7263 '',
7264 'update-nag'
7265 );
7266 }
7267
7268 /**
7269 * Enqueue connect requires scripts and styles.
7270 *
7271 * @author Vova Feldman (@svovaf)
7272 * @since 1.1.4
7273 */
7274 function _enqueue_connect_essentials() {
7275 wp_enqueue_script( 'jquery' );
7276 wp_enqueue_script( 'json2' );
7277
7278 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7279 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7280 }
7281
7282 /**
7283 * Add connect / opt-in pointer.
7284 *
7285 * @author Vova Feldman (@svovaf)
7286 * @since 1.1.4
7287 */
7288 function _add_connect_pointer_script() {
7289 $vars = array( 'id' => $this->_module_id );
7290 $pointer_content = fs_get_template( 'connect.php', $vars );
7291 ?>
7292 <script type="text/javascript">// <![CDATA[
7293 jQuery(document).ready(function ($) {
7294 if ('undefined' !== typeof(jQuery().pointer)) {
7295
7296 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7297
7298 if (element.length > 0) {
7299 var optin = $(element).pointer($.extend(true, {}, {
7300 content : <?php echo json_encode( $pointer_content ) ?>,
7301 position : {
7302 edge : 'left',
7303 align: 'center'
7304 },
7305 buttons : function () {
7306 // Don't show pointer buttons.
7307 return '';
7308 },
7309 pointerWidth: 482
7310 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7311
7312 <?php
7313 echo $this->apply_filters( 'optin_pointer_execute', "
7314
7315 optin.pointer('open');
7316
7317 // Tag the opt-in pointer with custom class.
7318 $('.wp-pointer #fs_connect')
7319 .parents('.wp-pointer.wp-pointer-top')
7320 .addClass('fs-opt-in-pointer');
7321
7322 ", 'element', 'optin' ) ?>
7323 }
7324 }
7325 });
7326 // ]]></script>
7327 <?php
7328 }
7329
7330 /**
7331 * Return current page's URL.
7332 *
7333 * @author Vova Feldman (@svovaf)
7334 * @since 1.0.7
7335 *
7336 * @return string
7337 */
7338 static function current_page_url() {
7339 $url = 'http';
7340
7341 if ( isset( $_SERVER["HTTPS"] ) ) {
7342 if ( $_SERVER["HTTPS"] == "on" ) {
7343 $url .= "s";
7344 }
7345 }
7346 $url .= "://";
7347 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7348 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7349 } else {
7350 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7351 }
7352
7353 return esc_url( $url );
7354 }
7355
7356 /**
7357 * Check if the current page is the plugin's main admin settings page.
7358 *
7359 * @author Vova Feldman (@svovaf)
7360 * @since 1.0.7
7361 *
7362 * @return bool
7363 */
7364 function _is_plugin_page() {
7365 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7366 fs_is_plugin_page( $this->_slug );
7367 }
7368
7369 /* Events
7370 ------------------------------------------------------------------------------------------------------------------*/
7371 /**
7372 * Delete site install from Database.
7373 *
7374 * @author Vova Feldman (@svovaf)
7375 * @since 1.0.1
7376 *
7377 * @param bool $store
7378 * @param int|null $blog_id Since 2.0.0
7379 *
7380 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7381 */
7382 function _delete_site( $store = true, $blog_id = null ) {
7383 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7384 }
7385
7386 /**
7387 * Delete site install from Database.
7388 *
7389 * @author Vova Feldman (@svovaf)
7390 * @since 1.2.2.7
7391 *
7392 * @param string $slug
7393 * @param string $module_type
7394 * @param bool $store
7395 * @param int|null $blog_id Since 2.0.0
7396 *
7397 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7398 */
7399 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7400 $sites = self::get_all_sites( $module_type, $blog_id );
7401
7402 $install_id = false;
7403
7404 if ( isset( $sites[ $slug ] ) ) {
7405 if ( is_object( $sites[ $slug ] ) ) {
7406 $install_id = $sites[ $slug ]->id;
7407 }
7408
7409 unset( $sites[ $slug ] );
7410
7411 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7412 }
7413
7414 return $install_id;
7415 }
7416
7417 /**
7418 * Delete user.
7419 *
7420 * @author Vova Feldman (@svovaf)
7421 * @since 2.0.0
7422 *
7423 * @param number $user_id
7424 * @param bool $store
7425 *
7426 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7427 */
7428 private static function delete_user( $user_id, $store = true ) {
7429 $users = self::get_all_users();
7430
7431 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7432 return false;
7433 }
7434
7435 unset( $users[ $user_id ] );
7436
7437 self::$_accounts->set_option( 'users', $users, $store );
7438
7439 return $user_id;
7440 }
7441
7442 /**
7443 * Delete plugin's plans information.
7444 *
7445 * @param bool $store Flush to Database if true.
7446 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7447 *
7448 * @author Vova Feldman (@svovaf)
7449 * @since 1.0.9
7450 */
7451 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7452 $this->_logger->entrance();
7453
7454 $plans = self::get_all_plans( $this->_module_type );
7455
7456 $plans_to_keep = array();
7457
7458 if ( $keep_associated_plans ) {
7459 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7460 foreach ( $plans_ids_to_keep as $plan_id ) {
7461 $plan = self::_get_plan_by_id( $plan_id );
7462 if ( is_object( $plan ) ) {
7463 $plans_to_keep[] = self::_encrypt_entity( $plan );
7464 }
7465 }
7466 }
7467
7468 if ( ! empty( $plans_to_keep ) ) {
7469 $plans[ $this->_slug ] = $plans_to_keep;
7470 } else {
7471 unset( $plans[ $this->_slug ] );
7472 }
7473
7474 $this->set_account_option( 'plans', $plans, $store );
7475 }
7476
7477 /**
7478 * Delete all plugin licenses.
7479 *
7480 * @author Vova Feldman (@svovaf)
7481 * @since 1.0.9
7482 *
7483 * @param bool $store
7484 */
7485 private function _delete_licenses( $store = true ) {
7486 $this->_logger->entrance();
7487
7488 $all_licenses = self::get_all_licenses();
7489
7490 unset( $all_licenses[ $this->_module_id ] );
7491
7492 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7493 }
7494
7495 /**
7496 * Check if Freemius was added on new plugin installation.
7497 *
7498 * @author Vova Feldman (@svovaf)
7499 * @since 1.1.5
7500 *
7501 * @return bool
7502 */
7503 function is_plugin_new_install() {
7504 return isset( $this->_storage->is_plugin_new_install ) &&
7505 $this->_storage->is_plugin_new_install;
7506 }
7507
7508 /**
7509 * Check if it's the first plugin release that is running Freemius.
7510 *
7511 * @author Vova Feldman (@svovaf)
7512 * @since 1.2.1.5
7513 *
7514 * @return bool
7515 */
7516 function is_first_freemius_powered_version() {
7517 return empty( $this->_storage->plugin_last_version );
7518 }
7519
7520 /**
7521 * @author Leo Fajardo (@leorw)
7522 * @since 1.2.2
7523 *
7524 * @return bool|string
7525 */
7526 private function get_previous_theme_slug() {
7527 return isset( $this->_storage->previous_theme ) ?
7528 $this->_storage->previous_theme :
7529 false;
7530 }
7531
7532 /**
7533 * @author Leo Fajardo (@leorw)
7534 * @since 1.2.2
7535 *
7536 * @return bool
7537 */
7538 private function can_activate_previous_theme() {
7539 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7540 }
7541
7542 /**
7543 * @author Leo Fajardo (@leorw)
7544 * @since 2.5.0
7545 *
7546 * @return bool
7547 */
7548 private function can_activate_theme( $slug ) {
7549 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7550 $theme_instance = wp_get_theme( $slug );
7551
7552 return $theme_instance->exists();
7553 }
7554
7555 return false;
7556 }
7557
7558 /**
7559 * @author Leo Fajardo (@leorw)
7560 * @since 1.2.2
7561 */
7562 private function activate_previous_theme() {
7563 switch_theme( $this->get_previous_theme_slug() );
7564 unset( $this->_storage->previous_theme );
7565
7566 global $pagenow;
7567 if ( 'themes.php' === $pagenow ) {
7568 /**
7569 * Refresh the active theme information.
7570 *
7571 * @author Leo Fajardo (@leorw)
7572 * @since 1.2.2
7573 */
7574 fs_redirect( $this->admin_url( $pagenow ) );
7575 }
7576 }
7577
7578 /**
7579 * @author Leo Fajardo (@leorw)
7580 * @since 1.2.2
7581 *
7582 * @return string
7583 */
7584 function get_previous_theme_activation_url() {
7585 if ( ! $this->can_activate_previous_theme() ) {
7586 return '';
7587 }
7588
7589 /**
7590 * Activation URL
7591 *
7592 * @author Leo Fajardo (@leorw)
7593 * @since 1.2.2
7594 */
7595 return wp_nonce_url(
7596 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7597 'switch-theme_' . $this->get_previous_theme_slug()
7598 );
7599 }
7600
7601 /**
7602 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7603 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7604 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7605 * theme doesn't exist, then there will be no close button.
7606 *
7607 * @author Leo Fajardo (@leorw)
7608 * @since 1.2.2
7609 *
7610 * @param string $slug_or_name Old theme's slug or name.
7611 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7612 */
7613 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7614 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7615 $old_theme->get_stylesheet() :
7616 $slug_or_name;
7617
7618 $this->_activate_plugin_event_hook();
7619 }
7620
7621 /**
7622 * Plugin activated hook.
7623 *
7624 * @author Vova Feldman (@svovaf)
7625 * @since 1.0.1
7626 *
7627 * @uses FS_Api
7628 */
7629 function _activate_plugin_event_hook() {
7630 $this->_logger->entrance( 'slug = ' . $this->_slug );
7631
7632 if ( ! $this->is_user_admin() ) {
7633 return;
7634 }
7635
7636 $this->unregister_uninstall_hook();
7637
7638 // Clear API cache on activation.
7639 FS_Api::clear_cache();
7640
7641 $is_premium_version_activation = $this->is_plugin() ?
7642 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7643 $this->is_premium();
7644
7645 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7646 $this->clear_pending_activation_mode();
7647 }
7648
7649 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7650
7651 if ( $this->is_plugin() ) {
7652 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7653 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7654 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7655 $other_version_basename = $is_premium_version_activation ?
7656 $this->_free_plugin_basename :
7657 $this->premium_plugin_basename();
7658
7659 if ( ! $this->_is_network_active ) {
7660 /**
7661 * Themes are always network activated, but the ACTUAL activation is per site.
7662 *
7663 * During the activation, the plugin isn't yet active, therefore,
7664 * _is_network_active will be set to false even if it's a network level
7665 * activation. So we need to fix that by looking at the is_network_admin() value.
7666 *
7667 * @author Vova Feldman
7668 */
7669 $this->_is_network_active = (
7670 $this->_is_multisite_integrated &&
7671 fs_is_network_admin()
7672 );
7673 }
7674
7675 /**
7676 * If the other module version is active, deactivate it.
7677 *
7678 * is_plugin_active() checks if the plugin is active on the site or the network level and
7679 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7680 *
7681 * @author Leo Fajardo (@leorw)
7682 * @since 1.2.2
7683 */
7684 if (
7685 is_plugin_active( $other_version_basename ) &&
7686 $this->apply_filters( 'deactivate_on_activation', true )
7687 ) {
7688 deactivate_plugins( $other_version_basename );
7689 }
7690 }
7691
7692 if ( $this->is_registered() ) {
7693 if ( $is_premium_version_activation ) {
7694 $this->reconnect_locally();
7695 }
7696
7697
7698 // Schedule re-activation event and sync.
7699 // $this->sync_install( array(), true );
7700 $this->schedule_install_sync();
7701
7702 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7703 if ( $is_premium_version_activation ) {
7704 $this->_admin_notices->add(
7705 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7706 $this->get_text_x_inline( 'W00t',
7707 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7708 );
7709 }
7710 } else if ( $this->is_anonymous() ) {
7711 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7712 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7713 $network = true;
7714 } else {
7715 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7716 $this->_storage->is_anonymous['version'] :
7717 null;
7718 $network = false;
7719 }
7720
7721 /**
7722 * Reset "skipped" click cache on the following:
7723 * 1. Freemius DEV mode.
7724 * 2. WordPress DEBUG mode.
7725 * 3. If a plugin and the user skipped the exact same version before.
7726 *
7727 * @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).
7728 *
7729 * @todo 4. If explicitly asked to retry after every activation.
7730 */
7731 if ( WP_FS__DEV_MODE ||
7732 (
7733 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7734 $this->get_plugin_version() == $plugin_version
7735 )
7736 ) {
7737 $this->reset_anonymous_mode( $network );
7738 }
7739 }
7740
7741 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7742
7743 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7744 /**
7745 * When activating an add-on, try to also activate a license.
7746 *
7747 * @author Leo Fajardo (@leorw)
7748 * @since 2.3.0
7749 */
7750 if ( ! $this->_is_network_active ) {
7751 $this->maybe_activate_addon_license();
7752 } else {
7753 $this->maybe_network_activate_addon_license();
7754 }
7755
7756 /**
7757 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7758 *
7759 * @author Leo Fajardo (@leorw)
7760 * @since 2.3.0
7761 */
7762 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7763
7764 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7765 $this->_storage->require_license_activation = false;
7766 }
7767 }
7768
7769 if (
7770 $is_premium_version_activation &&
7771 (
7772 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7773 (
7774 $this->is_registered() &&
7775 ! $is_trial_or_has_features_enabled_license
7776 )
7777 )
7778 ) {
7779 $this->_storage->require_license_activation = true;
7780 }
7781
7782 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7783 /**
7784 * If no previous version of plugin's version exist, it means that it's either
7785 * the first time that the plugin installed on the site, or the plugin was installed
7786 * before but didn't have Freemius integrated.
7787 *
7788 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7789 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7790 * only after immediate activation.
7791 *
7792 * @since 1.1.4
7793 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7794 */
7795 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7796 }
7797
7798 /**
7799 * Also flush when activating the premium version so that even if Freemius was off before, the API
7800 * connectivity test can be run again.
7801 *
7802 * @author Leo Fajardo (@leorw)
7803 * @since 2.2.3.1
7804 */
7805 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7806
7807 if ( ! $this->_anonymous_mode &&
7808 ( false !== $has_api_connectivity ) &&
7809 ! $this->_isAutoInstall
7810 ) {
7811 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7812 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7813 }
7814
7815 /**
7816 * Activation hook is executed after the plugin's main file is loaded, therefore,
7817 * after the plugin was loaded. The logic is located at activate_plugin()
7818 * ./wp-admin/includes/plugin.php.
7819 *
7820 * @author Vova Feldman (@svovaf)
7821 * @since 1.1.9
7822 */
7823 $this->_storage->was_plugin_loaded = true;
7824 }
7825
7826 /**
7827 * @author Leo Fajardo (@leorw)
7828 * @since 2.3.0
7829 */
7830 private function maybe_activate_addon_license() {
7831 $parent_fs = $this->get_parent_instance();
7832
7833 if (
7834 ! is_object( $parent_fs ) ||
7835 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7836 ) {
7837 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7838 return;
7839 }
7840
7841 $license = $this->get_active_parent_license();
7842 if ( ! is_object( $license ) ) {
7843 return;
7844 }
7845
7846 if (
7847 $this->is_bundle_license_auto_activation_enabled() &&
7848 ! empty( $license->products )
7849 ) {
7850 $this->activate_bundle_license( $license );
7851
7852 return;
7853 }
7854
7855 if ( ! $this->is_registered() ) {
7856 // Opt in with a license key.
7857 $this->opt_in(
7858 $parent_fs->get_current_or_network_user()->email,
7859 false,
7860 false,
7861 $license->secret_key
7862 );
7863 } else {
7864 // Activate the license.
7865 $install = $this->api_site_call(
7866 '/',
7867 'put',
7868 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7869 );
7870
7871 if ( ! FS_Api::is_api_error( $install ) ) {
7872 $this->_sync_addon_license( $this->get_id(), true );
7873 }
7874 }
7875 }
7876
7877 /**
7878 * @author Leo Fajardo (@leorw)
7879 * @since 2.3.0
7880 *
7881 * @param FS_Plugin_License $license
7882 */
7883 private function maybe_network_activate_addon_license( $license = null ) {
7884 $parent_fs = $this->get_parent_instance();
7885 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7886 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7887 return;
7888 }
7889
7890 $license = ( ! is_null( $license ) ) ?
7891 $license :
7892 $this->get_active_parent_license();
7893
7894 if ( ! is_object( $license ) ) {
7895 return;
7896 }
7897
7898 if (
7899 $this->is_bundle_license_auto_activation_enabled() &&
7900 ! empty( $license->products )
7901 ) {
7902 $this->activate_bundle_license( $license );
7903
7904 return;
7905 }
7906
7907 if ( ! $this->is_network_registered() ) {
7908 $sites = $this->get_sites_for_network_level_optin();
7909
7910 if ( count( $sites ) > $license->left() ) {
7911 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7912 return;
7913 }
7914
7915 // Opt in with a license key.
7916 $this->opt_in(
7917 $parent_fs->get_user()->email,
7918 false,
7919 false,
7920 $license->secret_key,
7921 false,
7922 false,
7923 false,
7924 null,
7925 $sites
7926 );
7927 } else {
7928 $blog_2_install_map = array();
7929 $site_ids = array();
7930
7931 $all_sites = Freemius::get_sites();
7932
7933 foreach ( $all_sites as $site ) {
7934 $blog_id = Freemius::get_site_blog_id( $site );
7935 $install = $this->get_install_by_blog_id( $blog_id );
7936
7937 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7938 // Skip license activation for installs that are already associated with a license.
7939 continue;
7940 }
7941
7942 if ( is_object( $install ) ) {
7943 $blog_2_install_map[ $blog_id ] = $install;
7944 } else {
7945 $site_ids[] = $blog_id;
7946 }
7947 }
7948
7949 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7950 return;
7951 }
7952
7953 $user = $this->get_current_or_network_user();
7954
7955 if ( ! empty( $blog_2_install_map ) ) {
7956 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7957
7958 if ( true !== $result ) {
7959 return;
7960 }
7961 }
7962
7963 if ( ! empty( $site_ids ) ) {
7964 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7965 }
7966 }
7967 }
7968
7969 /**
7970 * 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.
7971 *
7972 * @author Leo Fajardo (@leorw)
7973 * @since 2.4.0
7974 *
7975 * @param FS_Plugin_License $license
7976 * @param array $sites
7977 * @param int $blog_id
7978 */
7979 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
7980 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
7981 $license = $this->_license;
7982 }
7983
7984 if ( ! is_object( $license ) ) {
7985 return;
7986 }
7987
7988 $parent_license = ( ! empty( $license->products ) ) ?
7989 $license :
7990 $this->get_active_parent_license( $license->secret_key );
7991
7992 if ( is_object( $parent_license ) ) {
7993 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
7994 }
7995 }
7996
7997 /**
7998 * Try to activate a bundle license for all the bundle products installed on the site.
7999 * (1) If a child product install already has a license, the bundle license won't be activated.
8000 * (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.
8001 * (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.
8002 *
8003 * @author Leo Fajardo (@leorw)
8004 * @since 2.4.0
8005 *
8006 * @param FS_Plugin_License $license
8007 * @param array $sites
8008 * @param int $current_blog_id
8009 */
8010 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8011 $is_network_admin = fs_is_network_admin();
8012
8013 $installs_by_blog_map = array();
8014 $site_info_by_blog_map = array();
8015
8016 /**
8017 * Try to activate the license for all supported products.
8018 *
8019 * @author Leo Fajardo
8020 */
8021 foreach ( $license->products as $product_id ) {
8022 $fs = self::get_instance_by_id( $product_id );
8023
8024 if ( ! is_object( $fs ) ) {
8025 continue;
8026 }
8027
8028 if ( ! $fs->has_paid_plan() ) {
8029 continue;
8030 }
8031
8032 if (
8033 ! $fs->is_addon() &&
8034 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8035 ) {
8036 /**
8037 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8038 * there is a context bundle.
8039 */
8040 continue;
8041 }
8042
8043 if ( $current_blog_id > 0 ) {
8044 $fs->switch_to_blog( $current_blog_id );
8045 }
8046
8047 if ( $fs->has_active_valid_license() ) {
8048 continue;
8049 }
8050
8051 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8052 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8053 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8054 continue;
8055 }
8056 } else {
8057 if ( ! $fs->is_network_active() ) {
8058 // Do not try to activate the license in the network level if the product is not network active.
8059 continue;
8060 }
8061
8062 if ( $fs->is_network_delegated_connection() ) {
8063 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8064 continue;
8065 }
8066
8067 $has_install_with_license = false;
8068
8069 // 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.
8070 $filtered_sites = array();
8071
8072 if ( empty( $sites ) ) {
8073 $all_sites = self::get_sites();
8074
8075 foreach ( $all_sites as $site ) {
8076 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8077 }
8078 } else {
8079 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8080 foreach ( $sites as $site ) {
8081 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8082 continue;
8083 }
8084
8085 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8086 }
8087 }
8088
8089 foreach ( $sites as $site ) {
8090 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8091 continue;
8092 }
8093
8094 $blog_id = $site['blog_id'];
8095
8096 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8097 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8098 }
8099
8100 $installs = $installs_by_blog_map[ $blog_id ];
8101 $install = null;
8102
8103 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8104 $install = $installs[ $fs->get_slug() ];
8105
8106 if (
8107 is_object( $install ) &&
8108 (
8109 ! FS_Site::is_valid_id( $install->id ) ||
8110 ! FS_User::is_valid_id( $install->user_id ) ||
8111 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8112 )
8113 ) {
8114 $install = null;
8115 }
8116 }
8117
8118 if (
8119 is_object( $install ) &&
8120 FS_Plugin_License::is_valid_id( $install->license_id )
8121 ) {
8122 $has_install_with_license = true;
8123 break;
8124 }
8125
8126 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8127 // Site activation delegated, don't activate bundle license on the site in the network admin.
8128 continue;
8129 }
8130
8131 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8132 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8133 }
8134
8135 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8136 }
8137
8138 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8139 // 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.
8140 continue;
8141 }
8142
8143 $sites = $filtered_sites;
8144 }
8145
8146 $fs->activate_migrated_license(
8147 $license->secret_key,
8148 null,
8149 null,
8150 $sites,
8151 ( $current_blog_id > 0 ? $current_blog_id : null )
8152 );
8153 }
8154 }
8155
8156 /**
8157 * Returns a parent license that can be activated for the context product.
8158 *
8159 * @author Leo Fajardo (@leorw)
8160 * @since 2.3.0
8161 *
8162 * @param string|null $license_key
8163 * @param bool $flush
8164 *
8165 * @return FS_Plugin_License
8166 */
8167 function get_active_parent_license( $license_key = null, $flush = true ) {
8168 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8169
8170 $fs = $this;
8171
8172 if ( $this->is_addon() ) {
8173 $parent_instance = $this->get_parent_instance();
8174
8175 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8176 $fs = $parent_instance;
8177 }
8178 }
8179
8180 $foreign_licenses = $fs->get_foreign_licenses_info(
8181 self::get_all_licenses( $this->get_parent_id() )
8182 );
8183
8184 if ( ! empty ( $foreign_licenses ) ) {
8185 $foreign_licenses = array(
8186 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8187 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8188 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8189 );
8190
8191 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8192 }
8193
8194 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8195
8196 if (
8197 ! $this->is_api_result_object( $result, 'licenses' ) ||
8198 ! is_array( $result->licenses ) ||
8199 empty( $result->licenses )
8200 ) {
8201 return null;
8202 }
8203
8204 $parent_license = null;
8205
8206 if ( empty( $license_key ) ) {
8207 $parent_license = $result->licenses[0];
8208 } else {
8209 foreach ( $result->licenses as $license ) {
8210 if ( $license_key === $license->secret_key ) {
8211 $parent_license = $license;
8212 break;
8213 }
8214 }
8215 }
8216
8217 if ( ! is_null( $parent_license ) ) {
8218 $parent_license = new FS_Plugin_License( $parent_license );
8219 }
8220
8221 return $parent_license;
8222 }
8223
8224 /**
8225 * @author Leo Fajardo (@leorw)
8226 * @since 2.3.0
8227 *
8228 * @return array
8229 */
8230 function get_sites_for_network_level_optin() {
8231 $sites = array();
8232 $all_sites = self::get_sites();
8233
8234 foreach ( $all_sites as $site ) {
8235 $blog_id = self::get_site_blog_id( $site );
8236
8237 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8238 ! $this->is_installed_on_site( $blog_id )
8239 ) {
8240 $sites[] = $this->get_site_info( $site );
8241 }
8242 }
8243
8244 return $sites;
8245 }
8246
8247 /**
8248 * Delete account.
8249 *
8250 * @author Vova Feldman (@svovaf)
8251 * @since 1.0.3
8252 *
8253 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8254 */
8255 function delete_account_event( $check_user = true ) {
8256 $this->_logger->entrance( 'slug = ' . $this->_slug );
8257
8258 if ( $check_user && ! $this->is_user_admin() ) {
8259 return;
8260 }
8261
8262 $this->do_action( 'before_account_delete' );
8263
8264 // Clear all admin notices.
8265 $this->_admin_notices->clear_all_sticky( false );
8266
8267 $this->_delete_site( false );
8268
8269 $delete_network_common_data = true;
8270
8271 if ( $this->_is_network_active ) {
8272 $installs = $this->get_blog_install_map();
8273
8274 // Don't delete common network data unless no other installs left.
8275 $delete_network_common_data = empty( $installs );
8276 }
8277
8278 if ( $delete_network_common_data ) {
8279 $this->_delete_plans( false );
8280
8281 $this->_delete_licenses( false );
8282
8283 // Delete add-ons related to plugin's account.
8284 $this->_delete_account_addons( false );
8285 }
8286
8287 // @todo Delete plans and licenses of add-ons.
8288
8289 self::$_accounts->store();
8290
8291 /**
8292 * IMPORTANT:
8293 * Clear crons must be executed before clearing all storage.
8294 * Otherwise, the cron will not be cleared.
8295 */
8296 if ( $delete_network_common_data ) {
8297 $this->clear_sync_cron();
8298 }
8299
8300 $this->clear_install_sync_cron();
8301
8302 // Clear all storage data.
8303 $this->_storage->clear_all( true, array(
8304 'is_delegated_connection',
8305 'connectivity_test',
8306 'is_on',
8307 ), false );
8308
8309 // Send delete event.
8310 $this->get_api_site_scope()->call( '/', 'delete' );
8311
8312 $this->do_action( 'after_account_delete' );
8313 }
8314
8315 /**
8316 * Delete network level account.
8317 *
8318 * @author Vova Feldman (@svovaf)
8319 * @since 2.0.0
8320 *
8321 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8322 */
8323 function delete_network_account_event( $check_user = true ) {
8324 $this->_logger->entrance( 'slug = ' . $this->_slug );
8325
8326 if ( $check_user && ! $this->is_user_admin() ) {
8327 return;
8328 }
8329
8330 $this->do_action( 'before_network_account_delete' );
8331
8332 // Clear all admin notices.
8333 $this->_admin_notices->clear_all_sticky();
8334
8335 $this->_delete_plans( false, false );
8336
8337 $this->_delete_licenses( false );
8338
8339 // Delete add-ons related to plugin's account.
8340 $this->_delete_account_addons( false );
8341
8342 // @todo Delete plans and licenses of add-ons.
8343
8344 self::$_accounts->store( true );
8345
8346 /**
8347 * IMPORTANT:
8348 * Clear crons must be executed before clearing all storage.
8349 * Otherwise, the cron will not be cleared.
8350 */
8351 $this->clear_sync_cron( true );
8352 $this->clear_install_sync_cron( true );
8353
8354 $sites = self::get_sites();
8355
8356 $install_ids = array();
8357 foreach ( $sites as $site ) {
8358 $blog_id = self::get_site_blog_id( $site );
8359
8360 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8361 continue;
8362 }
8363
8364 $install_id = $this->_delete_site( true, $blog_id );
8365
8366 // Clear all storage data.
8367 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8368
8369 if ( FS_Site::is_valid_id( $install_id ) ) {
8370 $install_ids[] = $install_id;
8371 }
8372
8373 switch_to_blog( $blog_id );
8374
8375 $this->do_action( 'after_account_delete' );
8376
8377 restore_current_blog();
8378 }
8379
8380 $this->_storage->clear_all( true, array(
8381 'connectivity_test',
8382 'is_on',
8383 ), true );
8384
8385 // Send delete event.
8386 if ( ! empty( $install_ids ) ) {
8387 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8388 }
8389
8390 $this->do_action( 'after_network_account_delete' );
8391 }
8392
8393 /**
8394 * Plugin deactivation hook.
8395 *
8396 * @author Vova Feldman (@svovaf)
8397 * @since 1.0.1
8398 */
8399 function _deactivate_plugin_hook() {
8400 $this->_logger->entrance( 'slug = ' . $this->_slug );
8401
8402 if ( ! $this->is_user_admin() ) {
8403 return;
8404 }
8405
8406 $is_network_deactivation = fs_is_network_admin();
8407 $storage_keys_for_removal = array();
8408
8409 $this->_admin_notices->clear_all_sticky();
8410
8411 $storage_keys_for_removal[] = 'sticky_optin_added';
8412 if ( isset( $this->_storage->sticky_optin_added ) ) {
8413 unset( $this->_storage->sticky_optin_added );
8414 }
8415
8416 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8417 // Remember that plugin was already installed.
8418 $this->_storage->is_plugin_new_install = false;
8419 }
8420
8421 // Hook to plugin uninstall.
8422 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8423
8424 $this->clear_module_main_file_cache();
8425 $this->clear_sync_cron( $this->_is_network_active );
8426 $this->clear_install_sync_cron();
8427
8428 if ( $this->is_registered() ) {
8429 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8430 FS_Plugin_Updater::instance( $this )->delete_update_data();
8431 }
8432
8433 if ( $is_network_deactivation ) {
8434 // Send deactivation event.
8435 $this->sync_installs( array(
8436 'is_active' => false,
8437 ) );
8438 } else {
8439 // Send deactivation event.
8440 $this->sync_install( array(
8441 'is_active' => false,
8442 ) );
8443 }
8444 } else {
8445 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8446 // Reset connectivity test cache.
8447 $this->clear_connectivity_info();
8448
8449 $storage_keys_for_removal[] = 'connectivity_test';
8450 }
8451 }
8452
8453 if ( $is_network_deactivation ) {
8454 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8455 unset( $this->_storage->sticky_optin_added_ms );
8456 }
8457
8458 if ( ! empty( $storage_keys_for_removal ) ) {
8459 $sites = self::get_sites();
8460
8461 foreach ( $sites as $site ) {
8462 $blog_id = self::get_site_blog_id( $site );
8463
8464 foreach ( $storage_keys_for_removal as $key ) {
8465 $this->_storage->remove( $key, false, $blog_id );
8466 }
8467
8468 $this->_storage->save( $blog_id );
8469 }
8470 }
8471 }
8472
8473 // Clear API cache on deactivation.
8474 FS_Api::clear_cache();
8475
8476 $this->remove_sdk_reference();
8477 }
8478
8479 /**
8480 * @author Vova Feldman (@svovaf)
8481 * @since 1.1.6
8482 */
8483 private function remove_sdk_reference() {
8484 global $fs_active_plugins;
8485
8486 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8487 if ( $this->_plugin_basename == $data->plugin_path ) {
8488 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8489 break;
8490 }
8491 }
8492
8493 fs_fallback_to_newest_active_sdk();
8494 }
8495
8496 /**
8497 * @author Vova Feldman (@svovaf)
8498 * @since 1.1.3
8499 *
8500 * @param bool $is_anonymous
8501 * @param bool|int $network_or_blog_id Since 2.0.0
8502 */
8503 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8504 // Store information regarding skip to try and opt-in the user
8505 // again in the future.
8506 $skip_info = array(
8507 'is' => $is_anonymous,
8508 'timestamp' => WP_FS__SCRIPT_START_TIME,
8509 'version' => $this->get_plugin_version(),
8510 );
8511
8512 if ( true === $network_or_blog_id ) {
8513 $this->_storage->is_anonymous_ms = $skip_info;
8514 } else {
8515 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8516 }
8517
8518 $this->network_upgrade_mode_completed();
8519
8520 // Update anonymous mode cache.
8521 $this->_is_anonymous = $is_anonymous;
8522 }
8523
8524 /**
8525 * @author Vova Feldman (@svovaf)
8526 * @since 2.5.1
8527 *
8528 * @param bool|int $network_or_blog_id
8529 */
8530 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8531 if ( true === $network_or_blog_id ) {
8532 unset( $this->_storage->is_anonymous_ms );
8533 } else {
8534 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8535 }
8536 }
8537
8538 /**
8539 * @author Vova Feldman (@svovaf)
8540 * @since 2.0.0
8541 *
8542 * @param int $blog_id Site ID.
8543 * @param int $user_id User ID.
8544 * @param string $domain Site domain.
8545 * @param string $path Site path.
8546 * @param int $network_id Network ID. Only relevant on multi-network installations.
8547 * @param array $meta Metadata. Used to set initial site options.
8548 *
8549 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8550 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8551 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8552 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8553 */
8554 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8555 $this->_logger->entrance();
8556
8557 if ( ! $this->_is_network_active ) {
8558 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8559 return;
8560 }
8561
8562 $site = null;
8563 $new_blog_id = $blog_id;
8564
8565 if ( $this->is_premium() &&
8566 $this->is_network_connected() &&
8567 is_object( $this->_license ) &&
8568 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8569 $this->is_license_network_active( $blog_id )
8570 ) {
8571 /**
8572 * 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.
8573 */
8574 $current_blog_id = get_current_blog_id();
8575 $license = clone $this->_license;
8576
8577 $this->switch_to_blog( $blog_id );
8578
8579 // Opt-in with network user.
8580 $this->install_with_user(
8581 $this->get_network_user(),
8582 $license->secret_key,
8583 false,
8584 false,
8585 false
8586 );
8587
8588 if ( is_object( $this->_site ) ) {
8589 if ( $this->_site->license_id == $license->id ) {
8590 /**
8591 * If the license was activated successfully, sync the license data from the remote server.
8592 */
8593 $this->_license = $license;
8594 $this->sync_site_license();
8595 }
8596 }
8597
8598 $site = $this->_site;
8599
8600 $this->switch_to_blog( $current_blog_id );
8601
8602 if ( is_object( $site ) ) {
8603 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8604
8605 // Already connected (with or without a license), so no need to continue.
8606 return;
8607 }
8608 }
8609
8610 if ( $this->is_network_anonymous() ) {
8611 /**
8612 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8613 */
8614 $this->skip_site_connection( $blog_id );
8615 } else if ( $this->is_network_delegated_connection() ) {
8616 /**
8617 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8618 */
8619 $this->delegate_site_connection( $blog_id );
8620 } else if ( $this->is_network_connected() ) {
8621 /**
8622 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8623 */
8624 $current_blog_id = get_current_blog_id();
8625
8626 $this->switch_to_blog( $blog_id );
8627
8628 // Opt-in with network user.
8629 $this->install_with_user(
8630 $this->get_network_user(),
8631 false,
8632 false,
8633 false,
8634 false
8635 );
8636
8637 $site = $this->_site;
8638
8639 $this->switch_to_blog( $current_blog_id );
8640 } else {
8641 /**
8642 * If the super-admin mixed different options (connect, skip, delegated):
8643 * a) If at least one site connection was delegated, then automatically delegate connection.
8644 * 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.
8645 */
8646 $has_delegated_site = false;
8647
8648 $sites = self::get_sites();
8649 foreach ( $sites as $wp_site ) {
8650 $blog_id = self::get_site_blog_id( $wp_site );
8651
8652 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8653 $has_delegated_site = true;
8654 break;
8655 }
8656 }
8657
8658 if ( $has_delegated_site ) {
8659 $this->delegate_site_connection( $blog_id );
8660 } else {
8661 $this->skip_site_connection( $blog_id );
8662 }
8663 }
8664
8665 /**
8666 * 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.
8667 *
8668 * @author Leo Fajardo (@leorw)
8669 * @since 2.5.0
8670 */
8671 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8672 }
8673
8674 /**
8675 * @author Vova Feldman (@svovaf)
8676 * @since 2.5.0
8677 *
8678 * @param \WP_Site $new_site
8679 * @param array $args
8680 */
8681 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8682 $this->_logger->entrance();
8683
8684 $this->_after_new_blog_callback(
8685 $new_site->id,
8686 // Dummy user ID (not in use).
8687 0,
8688 $new_site->domain,
8689 $new_site->path,
8690 $new_site->network_id,
8691 // Dummy meta, not in use.
8692 array()
8693 );
8694 }
8695
8696 /**
8697 * @author Vova Feldman (@svovaf)
8698 * @since 1.1.3
8699 *
8700 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8701 */
8702 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8703 if ( true === $network_or_blog_ids ) {
8704 $this->unset_anonymous_mode( true );
8705
8706 if ( fs_is_network_admin() ) {
8707 $this->_is_anonymous = null;
8708 }
8709
8710 // Rest anonymous mode for all non-delegated sub-sites.
8711 $blog_ids = $this->get_non_delegated_blog_ids();
8712 }
8713 else
8714 {
8715 if ( false === $network_or_blog_ids ) {
8716 $network_or_blog_ids = 0;
8717 }
8718
8719 $blog_ids = is_array( $network_or_blog_ids ) ?
8720 $network_or_blog_ids :
8721 array( $network_or_blog_ids );
8722
8723 foreach ( $blog_ids as $blog_id ) {
8724 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8725 $this->_is_anonymous = null;
8726 }
8727 }
8728 }
8729
8730 foreach ( $blog_ids as $blog_id ) {
8731 $this->unset_anonymous_mode( $blog_id );
8732 }
8733
8734 /**
8735 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8736 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8737 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8738 *
8739 * @author Leo Fajardo (@leorw)
8740 * @since 1.2.2
8741 */
8742 if ( ! $this->_is_network_active ) {
8743 $this->_is_anonymous = null;
8744 }
8745 }
8746
8747 /**
8748 * @author Leo Fajardo (@leorw)
8749 * @since 2.5.3
8750 */
8751 private function update_license_required_permissions_if_anonymous() {
8752 if ( ! $this->is_anonymous() ) {
8753 return;
8754 }
8755
8756 $this->reset_anonymous_mode( fs_is_network_admin() );
8757
8758 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8759 'essentials' => true,
8760 'events' => true,
8761 'diagnostic' => false,
8762 'extensions' => false,
8763 'site' => false,
8764 ) );
8765 }
8766
8767 /**
8768 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8769 * deleting the account in the network level, the URL of the page to redirect to is correct.
8770 *
8771 * @author Leo Fajardo (@leorw)
8772 *
8773 * @since 2.1.3
8774 */
8775 private function maybe_set_slug_and_network_menu_exists_flag() {
8776 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8777 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8778 $this->_menu->get_slug() :
8779 $this->_slug
8780 );
8781 }
8782 }
8783
8784 /**
8785 * Clears the anonymous mode and redirects to the opt-in screen.
8786 *
8787 * @author Vova Feldman (@svovaf)
8788 * @since 1.1.7
8789 */
8790 function connect_again() {
8791 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8792 return;
8793 }
8794
8795 if ( $this->is_anonymous() ) {
8796 $this->reset_anonymous_mode( fs_is_network_admin() );
8797 }
8798
8799 $activation_url_params = array();
8800
8801 if ( $this->is_pending_activation() ) {
8802 $this->clear_pending_activation_mode();
8803
8804 if ( fs_request_get_bool( 'require_license' ) ) {
8805 $activation_url_params['require_license'] = true;
8806 }
8807 }
8808
8809 $this->maybe_set_slug_and_network_menu_exists_flag();
8810
8811 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8812 }
8813
8814 /**
8815 * Skip account connect, and set anonymous mode.
8816 *
8817 * @author Vova Feldman (@svovaf)
8818 * @since 1.1.1
8819 *
8820 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8821 */
8822 function skip_connection( $network_or_blog_ids = false ) {
8823 $this->_logger->entrance();
8824
8825 $this->_admin_notices->remove_sticky( 'connect_account' );
8826
8827 if ( true === $network_or_blog_ids ) {
8828 $this->set_anonymous_mode( true, true );
8829
8830 if ( fs_is_network_admin() ) {
8831 $this->_is_anonymous = null;
8832 }
8833
8834 // Rest anonymous mode for all non-delegated sub-sites.
8835 $blog_ids = $this->get_non_delegated_blog_ids();
8836 }
8837 else
8838 {
8839 if ( false === $network_or_blog_ids ) {
8840 $network_or_blog_ids = 0;
8841 }
8842
8843 $blog_ids = is_array( $network_or_blog_ids ) ?
8844 $network_or_blog_ids :
8845 array( $network_or_blog_ids );
8846
8847 foreach ( $blog_ids as $blog_id ) {
8848 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8849 $this->_is_anonymous = null;
8850 }
8851 }
8852 }
8853
8854 foreach ( $blog_ids as $blog_id ) {
8855 $this->skip_site_connection( $blog_id );
8856 }
8857
8858 $this->network_upgrade_mode_completed();
8859 }
8860
8861 /**
8862 * Skip connection for specific site in the network.
8863 *
8864 * @author Vova Feldman (@svovaf)
8865 * @since 2.0.0
8866 *
8867 * @param int|null $blog_id
8868 * @param bool $send_skip
8869 */
8870 private function skip_site_connection( $blog_id = null ) {
8871 $this->_logger->entrance();
8872
8873 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8874
8875 $this->set_anonymous_mode( true, $blog_id );
8876 }
8877
8878 /**
8879 * Plugin version update hook.
8880 *
8881 * @author Vova Feldman (@svovaf)
8882 * @since 1.0.4
8883 */
8884 private function update_plugin_version_event() {
8885 $this->_logger->entrance();
8886
8887 if ( ! $this->is_registered() ) {
8888 return;
8889 }
8890
8891 $this->schedule_install_sync();
8892 // $this->sync_install( array(), true );
8893 }
8894
8895 /**
8896 * Generate an MD5 signature of a plugins collection.
8897 * This helper methods used to identify changes in a plugins collection.
8898 *
8899 * @author Vova Feldman (@svovaf)
8900 * @since 2.0.0
8901 *
8902 * @param array [string]array $plugins
8903 *
8904 * @return string
8905 */
8906 private function get_plugins_thumbprint( $plugins ) {
8907 ksort( $plugins );
8908
8909 $thumbprint = '';
8910 foreach ( $plugins as $basename => $data ) {
8911 $thumbprint .= $data['slug'] . ',' .
8912 $data['Version'] . ',' .
8913 ( $data['is_active'] ? '1' : '0' ) . ';';
8914 }
8915
8916 return md5( $thumbprint );
8917 }
8918
8919 /**
8920 * Return a list of modified plugins since the last sync.
8921 *
8922 * Note:
8923 * There's no point to store a plugins counter since even if the number of
8924 * plugins didn't change, we still need to check if the versions are all the
8925 * same and the activity state is similar.
8926 *
8927 * @author Vova Feldman (@svovaf)
8928 * @since 1.1.8
8929 *
8930 * @return array|false
8931 */
8932 private function get_plugins_data_for_api() {
8933 // Alias.
8934 $site_active_plugins_option_name = 'active_plugins';
8935 $network_plugins_option_name = 'all_plugins';
8936
8937 /**
8938 * Collection of all site level active plugins.
8939 */
8940 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8941
8942 if ( ! is_object( $site_active_plugins_cache ) ) {
8943 $site_active_plugins_cache = (object) array(
8944 'timestamp' => '',
8945 'md5' => '',
8946 'plugins' => array(),
8947 );
8948 }
8949
8950 $time = time();
8951
8952 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8953 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8954 ) {
8955 // Don't send plugin updates if last update was in the past 5 min.
8956 return false;
8957 }
8958
8959 // Write timestamp to lock the logic.
8960 $site_active_plugins_cache->timestamp = $time;
8961 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8962
8963 // Reload options from DB.
8964 self::$_accounts->load( true );
8965 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8966
8967 if ( $time != $site_active_plugins_cache->timestamp ) {
8968 // If timestamp is different, then another thread captured the lock.
8969 return false;
8970 }
8971
8972 /**
8973 * Collection of all plugins (network level).
8974 */
8975 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8976
8977 if ( ! is_object( $network_plugins_cache ) ) {
8978 $network_plugins_cache = (object) array(
8979 'timestamp' => '',
8980 'md5' => '',
8981 'plugins' => array(),
8982 );
8983 }
8984
8985 // Check if there's a change in plugins.
8986 $network_plugins = self::get_network_plugins();
8987 $site_active_plugins = self::get_site_active_plugins();
8988
8989 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8990 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8991
8992 // Check if plugins status changed (version or active/inactive).
8993 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8994 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8995
8996 if ( ! $network_plugins_changed &&
8997 ! $site_active_plugins_changed
8998 ) {
8999 // No changes.
9000 return array();
9001 }
9002
9003 $plugins_update_data = array();
9004
9005 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
9006 if ( ! isset( $network_plugins[ $basename ] ) ) {
9007 // Plugin uninstalled.
9008 $uninstalled_plugin_data = $data;
9009 $uninstalled_plugin_data['is_active'] = false;
9010 $uninstalled_plugin_data['is_uninstalled'] = true;
9011 $plugins_update_data[] = $uninstalled_plugin_data;
9012
9013 unset( $network_plugins[ $basename ] );
9014
9015 unset( $network_plugins_cache->plugins[ $basename ] );
9016 unset( $site_active_plugins_cache->plugins[ $basename ] );
9017
9018 continue;
9019 }
9020
9021 $was_active = $data['is_active'] ||
9022 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9023 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
9024 $is_active = $network_plugins[ $basename ]['is_active'] ||
9025 ( isset( $site_active_plugins[ $basename ] ) &&
9026 $site_active_plugins[ $basename ]['is_active'] );
9027
9028 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9029 isset( $site_active_plugins[ $basename ] )
9030 ) {
9031 // Plugin was site level activated.
9032 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
9033 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
9034 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9035 ! isset( $site_active_plugins[ $basename ] )
9036 ) {
9037 // Plugin was site level deactivated.
9038 unset( $site_active_plugins_cache->plugins[ $basename ] );
9039 }
9040
9041 $prev_version = $data['version'];
9042 $current_version = $network_plugins[ $basename ]['Version'];
9043
9044 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9045 // Plugin activated or deactivated, or version changed.
9046
9047 if ( $was_active !== $is_active ) {
9048 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9049 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9050 }
9051 }
9052
9053 if ( $prev_version !== $current_version ) {
9054 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9055 }
9056
9057 $updated_plugin_data = $data;
9058 $updated_plugin_data['is_active'] = $is_active;
9059 $updated_plugin_data['version'] = $current_version;
9060 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9061 $plugins_update_data[] = $updated_plugin_data;
9062 }
9063 }
9064
9065 // Find new plugins that weren't yet seen before.
9066 foreach ( $network_plugins as $basename => $data ) {
9067 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9068 // New plugin.
9069 $new_plugin = array(
9070 'slug' => $data['slug'],
9071 'version' => $data['Version'],
9072 'title' => $data['Name'],
9073 'is_active' => $data['is_active'],
9074 'is_uninstalled' => false,
9075 );
9076
9077 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9078
9079 $is_site_level_active = (
9080 isset( $site_active_plugins[ $basename ] ) &&
9081 $site_active_plugins[ $basename ]['is_active']
9082 );
9083
9084 /**
9085 * If not network active, set the activity status based on the site-level plugin status.
9086 */
9087 if ( ! $new_plugin['is_active'] ) {
9088 $new_plugin['is_active'] = $is_site_level_active;
9089 }
9090
9091 $plugins_update_data[] = $new_plugin;
9092
9093 if ( isset( $site_active_plugins[ $basename ] ) ) {
9094 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9095 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9096 }
9097 }
9098 }
9099
9100 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9101 $site_active_plugins_cache->timestamp = $time;
9102 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9103
9104 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9105 $network_plugins_cache->timestamp = $time;
9106 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9107
9108 return $plugins_update_data;
9109 }
9110
9111 /**
9112 * Return a list of modified themes since the last sync.
9113 *
9114 * Note:
9115 * There's no point to store a themes counter since even if the number of
9116 * themes didn't change, we still need to check if the versions are all the
9117 * same and the activity state is similar.
9118 *
9119 * @author Vova Feldman (@svovaf)
9120 * @since 1.1.8
9121 *
9122 * @return array|false
9123 */
9124 private function get_themes_data_for_api() {
9125 // Alias.
9126 $option_name = 'all_themes';
9127
9128 $all_cached_themes = self::$_accounts->get_option( $option_name );
9129
9130 if ( ! is_object( $all_cached_themes ) ) {
9131 $all_cached_themes = (object) array(
9132 'timestamp' => '',
9133 'md5' => '',
9134 'themes' => array(),
9135 );
9136 }
9137
9138 $time = time();
9139
9140 if ( ! empty( $all_cached_themes->timestamp ) &&
9141 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9142 ) {
9143 // Don't send theme updates if last update was in the past 5 min.
9144 return false;
9145 }
9146
9147 // Write timestamp to lock the logic.
9148 $all_cached_themes->timestamp = $time;
9149 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9150
9151 // Reload options from DB.
9152 self::$_accounts->load( true );
9153 $all_cached_themes = self::$_accounts->get_option( $option_name );
9154
9155 if ( $time != $all_cached_themes->timestamp ) {
9156 // If timestamp is different, then another thread captured the lock.
9157 return false;
9158 }
9159
9160 // Get active theme.
9161 $active_theme = wp_get_theme();
9162 $active_theme_stylesheet = $active_theme->get_stylesheet();
9163
9164 // Check if there's a change in themes.
9165 $all_themes = wp_get_themes();
9166
9167 // Check if themes changed.
9168 ksort( $all_themes );
9169
9170 $themes_signature = '';
9171 foreach ( $all_themes as $slug => $data ) {
9172 $is_active = ( $slug === $active_theme_stylesheet );
9173 $themes_signature .= $slug . ',' .
9174 $data->version . ',' .
9175 ( $is_active ? '1' : '0' ) . ';';
9176 }
9177
9178 // Check if themes status changed (version or active/inactive).
9179 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9180
9181 $themes_update_data = array();
9182
9183 if ( $themes_changed ) {
9184 // Change in themes, report changes.
9185
9186 // Update existing themes info.
9187 foreach ( $all_cached_themes->themes as $slug => $data ) {
9188 $is_active = ( $slug === $active_theme_stylesheet );
9189
9190 if ( ! isset( $all_themes[ $slug ] ) ) {
9191 // Plugin uninstalled.
9192 $uninstalled_theme_data = $data;
9193 $uninstalled_theme_data['is_active'] = false;
9194 $uninstalled_theme_data['is_uninstalled'] = true;
9195 $themes_update_data[] = $uninstalled_theme_data;
9196
9197 unset( $all_themes[ $slug ] );
9198 unset( $all_cached_themes->themes[ $slug ] );
9199 } else if ( $data['is_active'] !== $is_active ||
9200 $data['version'] !== $all_themes[ $slug ]->version
9201 ) {
9202 // Plugin activated or deactivated, or version changed.
9203
9204 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9205 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9206
9207 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9208 }
9209 }
9210
9211 // Find new themes that weren't yet seen before.
9212 foreach ( $all_themes as $slug => $data ) {
9213 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9214 $is_active = ( $slug === $active_theme_stylesheet );
9215
9216 // New plugin.
9217 $new_plugin = array(
9218 'slug' => $slug,
9219 'version' => $data->version,
9220 'title' => $data->name,
9221 'is_active' => $is_active,
9222 'is_uninstalled' => false,
9223 );
9224
9225 $themes_update_data[] = $new_plugin;
9226 $all_cached_themes->themes[ $slug ] = $new_plugin;
9227 }
9228 }
9229
9230 $all_cached_themes->md5 = md5( $themes_signature );
9231 $all_cached_themes->timestamp = time();
9232 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9233 }
9234
9235 return $themes_update_data;
9236 }
9237
9238 /**
9239 * Get site data for API install request.
9240 *
9241 * @author Vova Feldman (@svovaf)
9242 * @since 1.1.2
9243 *
9244 * @param string[] $override
9245 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9246 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9247 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9248 *
9249 * @return array
9250 */
9251 private function get_install_data_for_api(
9252 array $override,
9253 $include_plugins = true,
9254 $include_themes = true,
9255 $include_blog_data = true
9256 ) {
9257 // Alias.
9258 $permissions = FS_Permission_Manager::instance( $this );
9259
9260 if ( $permissions->is_extensions_tracking_allowed() ) {
9261 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9262 /**
9263 * @since 1.1.8 Also send plugin updates.
9264 */
9265 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9266 $plugins = $this->get_plugins_data_for_api();
9267 if ( ! empty( $plugins ) ) {
9268 $override['plugins'] = $plugins;
9269 }
9270 }
9271 }
9272
9273 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9274 /**
9275 * @since 1.1.8 Also send themes updates.
9276 */
9277 if ( $include_themes && ! isset( $override['themes'] ) ) {
9278 $themes = $this->get_themes_data_for_api();
9279 if ( ! empty( $themes ) ) {
9280 $override['themes'] = $themes;
9281 }
9282 }
9283 }
9284 }
9285
9286 $versions = $this->get_versions();
9287
9288 $blog_data = array();
9289 if ( $include_blog_data ) {
9290 $blog_data['url'] = self::get_unfiltered_site_url();
9291
9292 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9293 $blog_data = array_merge( $blog_data, array(
9294 'language' => self::get_sanitized_language(),
9295 'title' => get_bloginfo( 'name' ),
9296 ) );
9297 }
9298 }
9299
9300 return array_merge( $versions, $blog_data, array(
9301 'version' => $this->get_plugin_version(),
9302 'is_premium' => $this->is_premium(),
9303 // Special params.
9304 'is_active' => true,
9305 'is_uninstalled' => false,
9306 ), $override );
9307 }
9308
9309 /**
9310 * Update installs details.
9311 *
9312 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9313 *
9314 * @author Vova Feldman (@svovaf)
9315 * @since 2.0.0
9316 *
9317 * @param string[] string $override
9318 * @param bool $only_diff
9319 * @param bool $is_keepalive
9320 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9321 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9322 *
9323 * @return array
9324 */
9325 private function get_installs_data_for_api(
9326 array $override,
9327 $only_diff = false,
9328 $is_keepalive = false,
9329 $include_plugins = true,
9330 $include_themes = true
9331 ) {
9332 /**
9333 * @since 1.1.8 Also send plugin updates.
9334 */
9335 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9336 // $plugins = $this->get_plugins_data_for_api();
9337 // if ( ! empty( $plugins ) ) {
9338 // $override['plugins'] = $plugins;
9339 // }
9340 // }
9341 /**
9342 * @since 1.1.8 Also send themes updates.
9343 */
9344 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9345 // $themes = $this->get_themes_data_for_api();
9346 // if ( ! empty( $themes ) ) {
9347 // $override['themes'] = $themes;
9348 // }
9349 // }
9350
9351 // Common properties.
9352 $versions = $this->get_versions();
9353 $common = array_merge( $versions, array(
9354 'version' => $this->get_plugin_version(),
9355 'is_premium' => $this->is_premium(),
9356 ), $override );
9357
9358
9359 $is_common_diff_for_any_site = false;
9360 $common_diff_union = array();
9361
9362 $installs_data = array();
9363
9364 $sites = self::get_sites();
9365
9366 $subsite_data_for_api_by_install_id = array();
9367 $install_url_by_install_id = array();
9368 $subsite_registration_date_by_install_id = array();
9369
9370 foreach ( $sites as $site ) {
9371 $blog_id = self::get_site_blog_id( $site );
9372
9373 $install = $this->get_install_by_blog_id( $blog_id );
9374
9375 if ( is_object( $install ) ) {
9376 if ( $install->user_id != $this->_user->id ) {
9377 // Install belongs to a different owner.
9378 continue;
9379 }
9380
9381 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9382 // Don't send updates regarding opted-out installs.
9383 continue;
9384 }
9385
9386 $install_data = $this->get_site_info( $site, true );
9387
9388 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9389 continue;
9390 }
9391
9392 $uid = $install_data['uid'];
9393 $url = $install_data['url'];
9394 $registration_date = $install_data['registration_date'];
9395
9396 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9397 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9398 $clone_install_url = $install_url_by_install_id[ $install->id ];
9399 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9400
9401 $skip = false;
9402
9403 if (
9404 ! empty( $install_data['registration_date'] ) &&
9405 ! empty( $clone_subsite_registration_date )
9406 ) {
9407 /**
9408 * 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.
9409 *
9410 * @author Leo Fajardo (@leorw)
9411 * @since 2.5.1
9412 */
9413 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9414 } else if (
9415 /**
9416 * 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.
9417 *
9418 * @author Leo Fajardo (@leorw)
9419 * @since 2.5.0
9420 */
9421 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9422 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9423 ) {
9424 $skip = true;
9425 }
9426
9427 if ( $skip ) {
9428 // 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.
9429 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9430 continue;
9431 }
9432 }
9433
9434 unset( $install_data['blog_id'] );
9435 unset( $install_data['uid'] );
9436 unset( $install_data['url'] );
9437 unset( $install_data['registration_date'] );
9438
9439 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9440 $install_data['is_uninstalled'] = $install->is_uninstalled;
9441
9442 $common_diff = null;
9443 $is_common_diff = false;
9444 if ( $only_diff ) {
9445 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9446 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9447
9448 $is_common_diff = ! empty( $common_diff );
9449
9450 if ( $is_common_diff ) {
9451 foreach ( $common_diff as $k => $v ) {
9452 if ( ! isset( $common_diff_union[ $k ] ) ) {
9453 $common_diff_union[ $k ] = $v;
9454 }
9455 }
9456 }
9457
9458 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9459 }
9460
9461 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9462 // Add install ID and site unique ID.
9463 $install_data['id'] = $install->id;
9464 $install_data['uid'] = $uid;
9465 $install_data['url'] = $url;
9466
9467 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9468 $install_url_by_install_id[ $install->id ] = $install->url;
9469 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9470 }
9471 }
9472 }
9473
9474 restore_current_blog();
9475
9476 $installs_data = array_merge(
9477 $installs_data,
9478 array_values( $subsite_data_for_api_by_install_id )
9479 );
9480
9481 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9482 if ( ! $only_diff ) {
9483 $installs_data[] = $common;
9484 } else if ( ! empty( $common_diff_union ) ) {
9485 $installs_data[] = $common_diff_union;
9486 }
9487 }
9488
9489 foreach ( $installs_data as &$data ) {
9490 $data = (object) $data;
9491 }
9492
9493 return $installs_data;
9494 }
9495
9496 /**
9497 * Compare site actual data to the stored install data and return the differences for an API data sync.
9498 *
9499 * @author Vova Feldman (@svovaf)
9500 * @since 2.0.0
9501 *
9502 * @param array $site
9503 * @param FS_Site $install
9504 * @param string[] string $override
9505 *
9506 * @return array
9507 */
9508 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9509 $diff = array();
9510 $special = array();
9511 $special_override = false;
9512
9513 foreach ( $site as $p => $v ) {
9514 if ( property_exists( $install, $p ) ) {
9515 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9516 $install->{$p} != $v
9517 ) {
9518 $val = self::get_api_sanitized_property( $p, $v );
9519
9520 if ( $install->{$p} != $val ) {
9521 $install->{$p} = $val;
9522 $diff[ $p ] = $val;
9523 }
9524 }
9525 } else {
9526 $special[ $p ] = $v;
9527
9528 if ( isset( $override[ $p ] ) ||
9529 'plugins' === $p ||
9530 'themes' === $p
9531 ) {
9532 $special_override = true;
9533 }
9534 }
9535 }
9536
9537 if ( $special_override || 0 < count( $diff ) ) {
9538 // Add special params only if has at least one
9539 // standard param, or if explicitly requested to
9540 // override a special param or a param which is not exist
9541 // in the install object.
9542 $diff = array_merge( $diff, $special );
9543 }
9544
9545 return $diff;
9546 }
9547
9548 /**
9549 * @author Leo Fajardo (@leorw)
9550 * @since 2.5.1
9551 */
9552 private function send_pending_clone_update_once() {
9553 $this->_logger->entrance();
9554
9555 if ( ! empty( $this->_storage->clone_id ) ) {
9556 return;
9557 }
9558
9559 $install_clone = $this->get_api_site_scope()->call(
9560 '/clones',
9561 'post',
9562 array( 'site_url' => self::get_unfiltered_site_url() )
9563 );
9564
9565 if ( $this->is_api_result_entity( $install_clone ) ) {
9566 $this->_storage->clone_id = $install_clone->id;
9567 }
9568 }
9569
9570 /**
9571 * @author Leo Fajardo (@leorw)
9572 * @since 2.5.1
9573 *
9574 * @param string $resolution_type
9575 * @param FS_Site $clone_context_install
9576 */
9577 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9578 $this->_logger->entrance();
9579
9580 if ( empty( $this->_storage->clone_id ) ) {
9581 return;
9582 }
9583
9584 $new_install_id = null;
9585 $current_site = null;
9586
9587 $flush = false;
9588
9589 /**
9590 * 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.
9591 */
9592 if ( $clone_context_install->id != $this->_site->id ) {
9593 $new_install_id = $this->_site->id;
9594 $current_site = $this->_site;
9595 $this->_site = $clone_context_install;
9596
9597 $flush = true;
9598 }
9599
9600 $this->get_api_site_scope( $flush )->call(
9601 "/clones/{$this->_storage->clone_id}",
9602 'put',
9603 array(
9604 'resolution' => $resolution_type,
9605 'new_install_id' => $new_install_id,
9606 )
9607 );
9608
9609 if ( is_object( $current_site ) ) {
9610 /**
9611 * Ensure that the install scope entity is updated back to the previous install entity.
9612 */
9613 $this->_site = $current_site;
9614
9615 // Restore the previous install scope entity of the API.
9616 $this->get_api_site_scope( true );
9617 }
9618 }
9619
9620 /**
9621 * Update install only if changed.
9622 *
9623 * @author Vova Feldman (@svovaf)
9624 * @since 1.0.9
9625 *
9626 * @param string[] string $override
9627 * @param bool $flush
9628 * @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.
9629 *
9630 * @return false|object|string
9631 */
9632 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9633 $this->_logger->entrance();
9634
9635 $check_properties = $this->get_install_data_for_api( $override );
9636
9637 if ( $flush ) {
9638 $params = $check_properties;
9639 } else {
9640 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9641 }
9642
9643 if ( empty( $params ) ) {
9644 $keepalive_only_update = $this->should_send_keepalive_update();
9645
9646 if ( ! $keepalive_only_update ) {
9647 /**
9648 * There are no updates to send including keepalive.
9649 *
9650 * @author Leo Fajardo (@leorw)
9651 * @since 2.2.3
9652 */
9653 return false;
9654 }
9655 }
9656
9657 if ( $is_two_way_sync ) {
9658 /**
9659 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9660 *
9661 * @author Leo Fajardo (@leorw)
9662 * @since 2.2.3
9663 */
9664 if ( ! is_multisite() ) {
9665 // Update last install sync timestamp.
9666 $this->set_cron_execution_timestamp( 'install_sync' );
9667 }
9668
9669 $params['uid'] = $this->get_anonymous_id();
9670 }
9671
9672 $this->set_keepalive_timestamp();
9673
9674 // Send updated values to FS.
9675 $site = $this->api_site_call( '/', 'put', $params, true );
9676
9677 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9678 /**
9679 * Clear scheduled install sync after a two-way sync call.
9680 *
9681 * @author Leo Fajardo (@leorw)
9682 * @since 2.2.3
9683 */
9684 if ( ! is_multisite() ) {
9685 // I successfully sent install update, clear scheduled sync if exist.
9686 $this->clear_install_sync_cron();
9687 }
9688 }
9689
9690 return $site;
9691 }
9692
9693 /**
9694 * Update installs only if changed.
9695 *
9696 * @author Vova Feldman (@svovaf)
9697 * @since 2.0.0
9698 *
9699 * @param string[] string $override
9700 * @param bool $flush
9701 * @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.
9702 *
9703 * @return false|object|string
9704 */
9705 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9706 $this->_logger->entrance();
9707
9708 /**
9709 * Pass `true` to use the network level storage since the update is for many installs.
9710 *
9711 * @author Leo Fajardo (@leorw)
9712 * @since 2.2.3
9713 */
9714 $should_send_keepalive = $this->should_send_keepalive_update( true );
9715
9716 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9717
9718 if ( empty( $installs_data ) ) {
9719 return false;
9720 }
9721
9722 if ( $is_two_way_sync ) {
9723 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9724 $this->set_cron_execution_timestamp( 'install_sync' );
9725 }
9726
9727 /**
9728 * Pass `true` to use the network level storage since the update is for many installs.
9729 *
9730 * @author Leo Fajardo (@leorw)
9731 * @since 2.2.3
9732 */
9733 $this->set_keepalive_timestamp( true );
9734
9735 // Send updated values to FS.
9736 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9737
9738 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9739 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9740 $this->clear_install_sync_cron();
9741 }
9742
9743 return $result;
9744 }
9745
9746 /**
9747 * @author Leo Fajardo (@leorw)
9748 *
9749 * @param bool|null $use_network_level_storage
9750 *
9751 * @return bool
9752 */
9753 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9754 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9755
9756 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9757 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9758 return true;
9759 } else {
9760 // 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.
9761 return ( 7 == rand( 1, 7 ) );
9762 }
9763 }
9764
9765 /**
9766 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9767 *
9768 * @author Leo Fajardo (@leorw)
9769 * @since 2.3.2
9770 */
9771 private function maybe_sync_install_user() {
9772 if ( $this->_user->id == $this->_site->user_id ) {
9773 return;
9774 }
9775
9776 // Fetch user data and store if found.
9777 $this->sync_user_by_current_install();
9778 }
9779
9780 /**
9781 * Update install only if changed.
9782 *
9783 * @author Vova Feldman (@svovaf)
9784 * @since 1.0.9
9785 *
9786 * @param string[] string $override
9787 * @param bool $flush
9788 */
9789 function sync_install( $override = array(), $flush = false ) {
9790 $this->_logger->entrance();
9791
9792 $site = $this->send_install_update( $override, $flush, true );
9793
9794 if ( false === $site ) {
9795 // No sync required.
9796 return;
9797 }
9798
9799 if ( ! $this->is_api_result_entity( $site ) ) {
9800 // Failed to sync, don't update locally.
9801 return;
9802 }
9803
9804 $this->_site = new FS_Site( $site );
9805
9806 $this->_store_site( true );
9807 }
9808
9809 /**
9810 * Update install only if changed.
9811 *
9812 * @author Vova Feldman (@svovaf)
9813 * @since 1.0.9
9814 *
9815 * @param string[] string $override
9816 * @param bool $flush
9817 */
9818 private function sync_installs( $override = array(), $flush = false ) {
9819 $this->_logger->entrance();
9820
9821 $result = $this->send_installs_update( $override, $flush, true );
9822
9823 if ( false === $result ) {
9824 // No sync required.
9825 return;
9826 }
9827
9828 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9829 // Failed to sync, don't update locally.
9830 return;
9831 }
9832
9833 $address_to_blog_map = $this->get_address_to_blog_map();
9834
9835 foreach ( $result->installs as $install ) {
9836 $this->_site = new FS_Site( $install );
9837
9838 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9839 $blog_id = $address_to_blog_map[ $address ];
9840
9841 $this->_store_site( true, $blog_id );
9842 }
9843 }
9844
9845 /**
9846 * Track install's custom event.
9847 *
9848 * IMPORTANT:
9849 * Custom event tracking is currently only supported for specific clients.
9850 * If you are not one of them, please don't use this method. If you will,
9851 * the API will simply ignore your request based on the plugin ID.
9852 *
9853 * Need custom tracking for your plugin or theme?
9854 * If you are interested in custom event tracking please contact [email protected]
9855 * for further details.
9856 *
9857 * @author Vova Feldman (@svovaf)
9858 * @since 1.2.1
9859 *
9860 * @param string $name Event name.
9861 * @param array $properties Associative key/value array with primitive values only
9862 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9863 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9864 *
9865 * @return object|false Event data or FALSE on failure.
9866 *
9867 * @throws \Freemius_InvalidArgumentException
9868 */
9869 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9870 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9871
9872 if ( ! $this->is_registered() ) {
9873 return false;
9874 }
9875
9876 $event = array( 'type' => $name );
9877
9878 if ( is_numeric( $process_at ) && $process_at > time() ) {
9879 $event['process_at'] = $process_at;
9880 }
9881
9882 if ( $once ) {
9883 $event['once'] = true;
9884 }
9885
9886 if ( ! empty( $properties ) ) {
9887 // Verify associative array values are primitive.
9888 foreach ( $properties as $k => $v ) {
9889 if ( ! is_scalar( $v ) ) {
9890 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9891 }
9892 }
9893
9894 $event['properties'] = $properties;
9895 }
9896
9897 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9898
9899 return $this->is_api_error( $result ) ?
9900 false :
9901 $result;
9902 }
9903
9904 /**
9905 * Track install's custom event only once, but it still triggers the API call.
9906 *
9907 * IMPORTANT:
9908 * Custom event tracking is currently only supported for specific clients.
9909 * If you are not one of them, please don't use this method. If you will,
9910 * the API will simply ignore your request based on the plugin ID.
9911 *
9912 * Need custom tracking for your plugin or theme?
9913 * If you are interested in custom event tracking please contact [email protected]
9914 * for further details.
9915 *
9916 * @author Vova Feldman (@svovaf)
9917 * @since 1.2.1
9918 *
9919 * @param string $name Event name.
9920 * @param array $properties Associative key/value array with primitive values only
9921 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9922 *
9923 * @return object|false Event data or FALSE on failure.
9924 *
9925 * @throws \Freemius_InvalidArgumentException
9926 *
9927 * @user Freemius::track_event()
9928 */
9929 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9930 return $this->track_event( $name, $properties, $process_at, true );
9931 }
9932
9933 /**
9934 * Plugin uninstall hook.
9935 *
9936 * @author Vova Feldman (@svovaf)
9937 * @since 1.0.1
9938 *
9939 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9940 */
9941 function _uninstall_plugin_event( $check_user = true ) {
9942 $this->_logger->entrance( 'slug = ' . $this->_slug );
9943
9944 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9945 return;
9946 }
9947
9948 $params = array();
9949 $uninstall_reason = null;
9950 if ( isset( $this->_storage->uninstall_reason ) ) {
9951 $uninstall_reason = $this->_storage->uninstall_reason;
9952 $params['reason_id'] = $uninstall_reason->id;
9953 $params['reason_info'] = $uninstall_reason->info;
9954 }
9955
9956 if ( ! $this->is_registered() ) {
9957 // Send anonymous uninstall event only if user submitted a feedback.
9958 if ( isset( $uninstall_reason ) ) {
9959 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9960 $this->opt_in( false, false, false, false, true );
9961 } else {
9962 $params['uid'] = $this->get_anonymous_id();
9963 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9964 }
9965 }
9966 } else {
9967 $params = array_merge( $params, array(
9968 'is_active' => false,
9969 'is_uninstalled' => true,
9970 ) );
9971
9972 if ( $this->_is_network_active ) {
9973 // Send uninstall event.
9974 $this->send_installs_update( $params );
9975 } else {
9976 // Send uninstall event and handle the result.
9977 $this->sync_install( $params );
9978 }
9979 }
9980
9981 // @todo Decide if we want to delete plugin information from db.
9982 }
9983
9984 /**
9985 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9986 *
9987 * @author Vova Feldman (@svovaf)
9988 * @since 2.2.1
9989 *
9990 * @param string $is_premium
9991 * @param string $caller
9992 *
9993 * @return string
9994 */
9995 function set_basename( $is_premium, $caller ) {
9996 $basename = plugin_basename( $caller );
9997
9998 $current_basename = $is_premium ?
9999 $this->_premium_plugin_basename :
10000 $this->_free_plugin_basename;
10001
10002 if ( $current_basename == $basename ) {
10003 // Basename value set correctly.
10004 return;
10005 }
10006
10007 if ( $is_premium ) {
10008 $this->_premium_plugin_basename = $basename;
10009 } else {
10010 $this->_free_plugin_basename = $basename;
10011 }
10012
10013 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
10014
10015 register_activation_hook(
10016 $plugin_dir . $basename,
10017 array( &$this, '_activate_plugin_event_hook' )
10018 );
10019 }
10020
10021 /**
10022 * @author Vova Feldman (@svovaf)
10023 * @since 1.1.1
10024 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
10025 *
10026 * @return string
10027 */
10028 function premium_plugin_basename() {
10029 if ( ! isset( $this->_premium_plugin_basename ) ) {
10030 $this->_premium_plugin_basename = $this->is_premium() ?
10031 // The product is premium, so use the current basename.
10032 $this->_plugin_basename :
10033 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
10034 }
10035
10036 return $this->_premium_plugin_basename;
10037 }
10038
10039 /**
10040 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
10041 *
10042 * @author Vova Feldman (@svovaf)
10043 * @since 1.0.2
10044 */
10045 public static function _uninstall_plugin_hook() {
10046 self::_load_required_static();
10047
10048 self::$_static_logger->entrance();
10049
10050 if ( ! current_user_can( 'activate_plugins' ) ) {
10051 return;
10052 }
10053
10054 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
10055
10056 self::$_static_logger->info( 'plugin = ' . $plugin_file );
10057
10058 define( 'WP_FS__UNINSTALL_MODE', true );
10059
10060 $fs = self::get_instance_by_file( $plugin_file );
10061
10062 if ( is_object( $fs ) ) {
10063 $fs->remove_sdk_reference();
10064
10065 self::require_plugin_essentials();
10066
10067 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
10068 is_plugin_active( $fs->premium_plugin_basename() )
10069 ) {
10070 // Deleting Free or Premium plugin version while the other version still installed.
10071 return;
10072 }
10073
10074 if (
10075 ! $fs->is_clone() &&
10076 /**
10077 * 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).
10078 *
10079 * @author Leo Fajardo
10080 */
10081 ( ! is_object( $fs->_site ) || $fs->is_registered() )
10082 ) {
10083 $fs->_uninstall_plugin_event();
10084 }
10085
10086 $fs->do_action( 'after_uninstall' );
10087 }
10088 }
10089
10090 #----------------------------------------------------------------------------------
10091 #region Plugin Information
10092 #----------------------------------------------------------------------------------
10093
10094 /**
10095 * Load WordPress core plugin.php essential module.
10096 *
10097 * @author Vova Feldman (@svovaf)
10098 * @since 1.1.1
10099 */
10100 private static function require_plugin_essentials() {
10101 if ( ! function_exists( 'get_plugins' ) ) {
10102 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
10103
10104 require_once ABSPATH . 'wp-admin/includes/plugin.php';
10105 }
10106 }
10107
10108 /**
10109 * Load WordPress core pluggable.php module.
10110 *
10111 * @author Vova Feldman (@svovaf)
10112 * @since 1.1.2
10113 */
10114 private static function require_pluggable_essentials() {
10115 if ( ! function_exists( 'wp_get_current_user' ) ) {
10116 require_once ABSPATH . 'wp-includes/pluggable.php';
10117 }
10118 }
10119
10120 /**
10121 * Return plugin data.
10122 *
10123 * @author Vova Feldman (@svovaf)
10124 * @since 1.0.1
10125 *
10126 * @param bool $reparse_plugin_metadata
10127 *
10128 * @return array
10129 */
10130 function get_plugin_data( $reparse_plugin_metadata = false ) {
10131 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
10132 self::require_plugin_essentials();
10133
10134 if ( $this->is_plugin() ) {
10135 /**
10136 * @author Vova Feldman (@svovaf)
10137 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
10138 *
10139 * @link https://github.com/Freemius/wordpress-sdk/issues/77
10140 */
10141 $plugin_data = get_plugin_data(
10142 $this->_plugin_main_file_path,
10143 false,
10144 false
10145 );
10146 } else {
10147 $theme_data = wp_get_theme();
10148
10149 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
10150 $parent_theme = $theme_data->parent();
10151
10152 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
10153 $theme_data = $parent_theme;
10154 }
10155 }
10156
10157 $plugin_data = array(
10158 'Name' => $theme_data->get( 'Name' ),
10159 'Version' => $theme_data->get( 'Version' ),
10160 'Author' => $theme_data->get( 'Author' ),
10161 'Description' => $theme_data->get( 'Description' ),
10162 'PluginURI' => $theme_data->get( 'ThemeURI' ),
10163 );
10164 }
10165
10166 $this->_plugin_data = $plugin_data;
10167 }
10168
10169 return $this->_plugin_data;
10170 }
10171
10172 /**
10173 * @author Vova Feldman (@svovaf)
10174 * @since 1.0.1
10175 * @since 1.2.2.5 If slug not set load slug by module ID.
10176 *
10177 * @return string Plugin slug.
10178 */
10179 function get_slug() {
10180 if ( ! isset( $this->_slug ) ) {
10181 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10182 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10183 }
10184
10185 return $this->_slug;
10186 }
10187
10188 /**
10189 * @author Leo Fajardo (@leorw)
10190 * @since 2.2.1
10191 *
10192 * @return string
10193 */
10194 function get_premium_slug() {
10195 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10196 $this->_plugin->premium_slug :
10197 "{$this->_slug}-premium";
10198 }
10199
10200 /**
10201 * Retrieve the desired folder name for the product.
10202 *
10203 * @author Vova Feldman (@svovaf)
10204 * @since 1.2.1.7
10205 *
10206 * @return string Plugin slug.
10207 */
10208 function get_target_folder_name() {
10209 return $this->can_use_premium_code() ?
10210 $this->_plugin->premium_slug :
10211 $this->_slug;
10212 }
10213
10214 /**
10215 * @author Vova Feldman (@svovaf)
10216 * @since 1.0.1
10217 *
10218 * @return number Plugin ID.
10219 */
10220 function get_id() {
10221 return $this->_plugin->id;
10222 }
10223
10224 /**
10225 * @author Leo Fajardo (@leorw)
10226 * @since 2.2.4
10227 *
10228 * @return number|null Bundle ID.
10229 */
10230 function get_bundle_id() {
10231 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10232 $this->_plugin->bundle_id :
10233 null;
10234 }
10235
10236 /**
10237 * @author Vova Feldman (@svovaf)
10238 * @since 2.3.1
10239 *
10240 * @return string|null Bundle public key.
10241 */
10242 function get_bundle_public_key() {
10243 return isset( $this->_plugin->bundle_public_key ) ?
10244 $this->_plugin->bundle_public_key :
10245 null;
10246 }
10247
10248 /**
10249 * Get whether the SDK has been initiated in the context of a Bundle.
10250 *
10251 * This will return true, if `bundle_id` is present in the SDK init parameters.
10252 *
10253 * ```php
10254 * $my_fs = fs_dynamic_init( array(
10255 * // ...
10256 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10257 * 'bundle_public_key' => 'pk_XXXX',
10258 * ) );
10259 * ```
10260 *
10261 * @author Swashata Ghosh (@swashata)
10262 * @since 2.5.0
10263 *
10264 * @return bool True if we are running in bundle context, false otherwise.
10265 */
10266 private function has_bundle_context() {
10267 return ! is_null( $this->get_bundle_id() );
10268 }
10269
10270 /**
10271 * @author Vova Feldman (@svovaf)
10272 * @since 1.2.1.5
10273 *
10274 * @return string Freemius SDK version
10275 */
10276 function get_sdk_version() {
10277 return $this->version;
10278 }
10279
10280 /**
10281 * @author Vova Feldman (@svovaf)
10282 * @since 1.2.1.5
10283 *
10284 * @return number Parent plugin ID (if parent exist).
10285 */
10286 function get_parent_id() {
10287 return $this->is_addon() ?
10288 $this->get_parent_instance()->get_id() :
10289 $this->_plugin->id;
10290 }
10291
10292 /**
10293 * @author Vova Feldman (@svovaf)
10294 * @since 2.3.1
10295 *
10296 * @return string
10297 */
10298 function get_usage_tracking_terms_url() {
10299 return $this->apply_filters(
10300 'usage_tracking_terms_url',
10301 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10302 );
10303 }
10304
10305 /**
10306 * @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.
10307 * @since 2.5.8
10308 *
10309 * @return string
10310 */
10311 function get_license_activation_terms_url() {
10312 return $this->apply_filters(
10313 'license_activation_terms_url',
10314 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10315 );
10316 }
10317
10318 /**
10319 * @author Vova Feldman (@svovaf)
10320 * @since 2.3.1
10321 *
10322 * @return string
10323 */
10324 function get_eula_url() {
10325 return $this->apply_filters(
10326 'eula_url',
10327 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10328 );
10329 }
10330
10331 /**
10332 * @author Vova Feldman (@svovaf)
10333 * @since 1.0.1
10334 *
10335 * @return string Plugin public key.
10336 */
10337 function get_public_key() {
10338 return $this->_plugin->public_key;
10339 }
10340
10341 /**
10342 * Will be available only on sandbox mode.
10343 *
10344 * @author Vova Feldman (@svovaf)
10345 * @since 1.0.4
10346 *
10347 * @return mixed Plugin secret key.
10348 */
10349 function get_secret_key() {
10350 return $this->_plugin->secret_key;
10351 }
10352
10353 /**
10354 * @author Vova Feldman (@svovaf)
10355 * @since 1.1.1
10356 *
10357 * @return bool
10358 */
10359 function has_secret_key() {
10360 return ! empty( $this->_plugin->secret_key );
10361 }
10362
10363 /**
10364 * @author Vova Feldman (@svovaf)
10365 * @since 1.0.9
10366 *
10367 * @param string|bool $premium_suffix
10368 *
10369 * @return string
10370 */
10371 function get_plugin_name( $premium_suffix = false ) {
10372 $this->_logger->entrance();
10373
10374 /**
10375 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10376 *
10377 * @author Vova Feldman
10378 */
10379 if ( ! isset( $this->_plugin_name ) ) {
10380 // Name is not yet set.
10381 $this->set_name( $premium_suffix );
10382 } else if (
10383 ! empty( $premium_suffix ) &&
10384 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10385 ) {
10386 // Name is already set, but there's a change in the premium suffix.
10387 $this->set_name( $premium_suffix );
10388 }
10389
10390 return $this->_plugin_name;
10391 }
10392
10393 /**
10394 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10395 *
10396 * @author Vova Feldman (@svovaf)
10397 * @since 2.2.1
10398 *
10399 * @param string $premium_suffix
10400 */
10401 private function set_name( $premium_suffix = '' ) {
10402 $plugin_data = $this->get_plugin_data();
10403
10404 // Get name.
10405 $this->_plugin_name = $plugin_data['Name'];
10406
10407 if ( is_string( $premium_suffix ) ) {
10408 $premium_suffix = trim( $premium_suffix );
10409
10410 if ( ! empty( $premium_suffix ) ) {
10411 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10412 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10413 $suffix_len = strlen( $suffix );
10414
10415 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10416 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10417 ) {
10418 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10419 }
10420 }
10421 }
10422
10423 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10424 }
10425
10426 /**
10427 * @author Vova Feldman (@svovaf)
10428 * @since 1.0.0
10429 *
10430 * @param bool $reparse_plugin_metadata
10431 *
10432 * @return string
10433 */
10434 function get_plugin_version( $reparse_plugin_metadata = false ) {
10435 $this->_logger->entrance();
10436
10437 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10438
10439 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10440
10441 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10442 }
10443
10444 /**
10445 * @author Vova Feldman (@svovaf)
10446 * @since 1.2.1.7
10447 *
10448 * @return string
10449 */
10450 function get_plugin_title() {
10451 $this->_logger->entrance();
10452
10453 $title = $this->_plugin->title;
10454
10455 return $this->apply_filters( 'plugin_title', $title );
10456 }
10457
10458 /**
10459 * @author Vova Feldman (@svovaf)
10460 * @since 1.2.2.7
10461 *
10462 * @param bool $lowercase
10463 *
10464 * @return string
10465 */
10466 function get_module_label( $lowercase = false ) {
10467 $label = $this->is_addon() ?
10468 $this->get_text_inline( 'Add-On', 'addon' ) :
10469 ( $this->is_plugin() ?
10470 $this->get_text_inline( 'Plugin', 'plugin' ) :
10471 $this->get_text_inline( 'Theme', 'theme' ) );
10472
10473 if ( $lowercase ) {
10474 $label = strtolower( $label );
10475 }
10476
10477 return $label;
10478 }
10479
10480 /**
10481 * @author Vova Feldman (@svovaf)
10482 * @since 1.0.4
10483 *
10484 * @return string
10485 */
10486 function get_plugin_basename() {
10487 if ( ! isset( $this->_plugin_basename ) ) {
10488 if ( $this->is_plugin() ) {
10489 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10490 } else {
10491 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10492 }
10493 }
10494
10495 return $this->_plugin_basename;
10496 }
10497
10498 function get_plugin_folder_name() {
10499 $this->_logger->entrance();
10500
10501 $plugin_folder = $this->_plugin_basename;
10502
10503 while ( '.' !== dirname( $plugin_folder ) ) {
10504 $plugin_folder = dirname( $plugin_folder );
10505 }
10506
10507 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10508
10509 return $plugin_folder;
10510 }
10511
10512 #endregion ------------------------------------------------------------------
10513
10514 /* Account
10515 ------------------------------------------------------------------------------------------------------------------*/
10516
10517 /**
10518 * Find plugin's slug by plugin's basename.
10519 *
10520 * @author Vova Feldman (@svovaf)
10521 * @since 1.0.9
10522 *
10523 * @param string $plugin_base_name
10524 *
10525 * @return false|string
10526 */
10527 private static function find_slug_by_basename( $plugin_base_name ) {
10528 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10529
10530 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10531 return false;
10532 }
10533
10534 return $file_slug_map[ $plugin_base_name ];
10535 }
10536
10537 /**
10538 * Store the map between the plugin's basename to the slug.
10539 *
10540 * @author Vova Feldman (@svovaf)
10541 * @since 1.0.9
10542 */
10543 private function store_file_slug_map() {
10544 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10545
10546 if ( ! array( $file_slug_map ) ) {
10547 $file_slug_map = array();
10548 }
10549
10550 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10551 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10552 ) {
10553 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10554 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10555 }
10556 }
10557
10558 /**
10559 * @return array[number]FS_User
10560 */
10561 static function get_all_users() {
10562 $users = self::maybe_get_entities_account_option( 'users', array() );
10563
10564 if ( ! is_array( $users ) ) {
10565 $users = array();
10566 }
10567
10568 return $users;
10569 }
10570
10571 /**
10572 * @param string $module_type
10573 * @param null|int $blog_id Since 2.0.0
10574 *
10575 * @return array[string]FS_Site
10576 */
10577 private static function get_all_sites(
10578 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10579 $blog_id = null,
10580 $is_backup = false
10581 ) {
10582 $sites = self::get_account_option(
10583 ( $is_backup ? 'prev_' : '' ) . 'sites',
10584 $module_type,
10585 $blog_id
10586 );
10587
10588 if ( ! is_array( $sites ) ) {
10589 $sites = array();
10590 }
10591
10592 return $sites;
10593 }
10594
10595 /**
10596 * @author Leo Fajardo (@leorw)
10597 *
10598 * @since 1.2.2
10599 *
10600 * @param string $option_name
10601 * @param string $module_type
10602 * @param null|int $network_level_or_blog_id Since 2.0.0
10603 *
10604 * @return mixed
10605 */
10606 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10607 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10608 $option_name = $module_type . '_' . $option_name;
10609 }
10610
10611 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10612 }
10613
10614 /**
10615 * @author Leo Fajardo (@leorw)
10616 *
10617 * @since 1.2.2
10618 *
10619 * @param string $option_name
10620 * @param mixed $option_value
10621 * @param bool $store
10622 * @param null|int $network_level_or_blog_id Since 2.0.0
10623 */
10624 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10625 self::set_account_option_by_module(
10626 $this->_module_type,
10627 $option_name,
10628 $option_value,
10629 $store,
10630 $network_level_or_blog_id
10631 );
10632 }
10633
10634 /**
10635 * @author Vova Feldman (@svovaf)
10636 *
10637 * @since 1.2.2.7
10638 *
10639 * @param string $module_type
10640 * @param string $option_name
10641 * @param mixed $option_value
10642 * @param bool $store
10643 * @param null|int $network_level_or_blog_id Since 2.0.0
10644 */
10645 private static function set_account_option_by_module(
10646 $module_type,
10647 $option_name,
10648 $option_value,
10649 $store,
10650 $network_level_or_blog_id = null
10651 ) {
10652 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10653 $option_name = $module_type . '_' . $option_name;
10654 }
10655
10656 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10657 }
10658
10659 /**
10660 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10661 *
10662 * @author Leo Fajardo (@leorw)
10663 * @since 2.3.1
10664 *
10665 * @param string $option_name
10666 * @param mixed $default
10667 * @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).
10668 *
10669 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10670 */
10671 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10672 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10673
10674 $class_name = '';
10675
10676 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10677 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10678 }
10679
10680 switch ( $option_name ) {
10681 case 'plugins':
10682 case 'themes':
10683 case 'addons':
10684 $class_name = FS_Plugin::get_class_name();
10685 break;
10686 case 'users':
10687 $class_name = FS_User::get_class_name();
10688 break;
10689 case 'sites':
10690 $class_name = FS_Site::get_class_name();
10691 break;
10692 case 'licenses':
10693 case 'all_licenses':
10694 $class_name = FS_Plugin_License::get_class_name();
10695 break;
10696 case 'plans':
10697 $class_name = FS_Plugin_Plan::get_class_name();
10698 break;
10699 case 'updates':
10700 $class_name = FS_Plugin_Tag::get_class_name();
10701 break;
10702 }
10703
10704 if ( empty( $class_name ) ) {
10705 return $option;
10706 }
10707
10708 return fs_get_entities( $option, $class_name );
10709 }
10710
10711 /**
10712 * @author Vova Feldman (@svovaf)
10713 * @since 1.0.6
10714 *
10715 * @param number|null $module_id
10716 *
10717 * @return FS_Plugin_License[]
10718 */
10719 private static function get_all_licenses( $module_id = null ) {
10720 $licenses = self::get_account_option( 'all_licenses' );
10721
10722 if ( ! is_array( $licenses ) ) {
10723 $licenses = array();
10724 }
10725
10726 if ( is_null( $module_id ) ) {
10727 return $licenses;
10728 }
10729
10730 $licenses = isset( $licenses[ $module_id ] ) ?
10731 $licenses[ $module_id ] :
10732 array();
10733
10734 return $licenses;
10735 }
10736
10737 /**
10738 * @author Leo Fajardo (@leorw)
10739 * @since 2.0.0
10740 *
10741 * @return array
10742 */
10743 private static function get_all_licenses_by_module_type() {
10744 $licenses = self::get_account_option( 'all_licenses' );
10745
10746 $licenses_by_module_type = array(
10747 WP_FS__MODULE_TYPE_PLUGIN => array(),
10748 WP_FS__MODULE_TYPE_THEME => array()
10749 );
10750
10751 if ( ! is_array( $licenses ) ) {
10752 return $licenses_by_module_type;
10753 }
10754
10755 foreach ( $licenses as $module_id => $module_licenses ) {
10756 $fs = self::get_instance_by_id( $module_id );
10757 if ( false === $fs ) {
10758 continue;
10759 }
10760
10761 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10762 }
10763
10764 return $licenses_by_module_type;
10765 }
10766
10767 /**
10768 * @author Leo Fajardo (@leorw)
10769 * @since 2.0.0
10770 *
10771 * @param number $module_id
10772 * @param number|null $user_id
10773 *
10774 * @return array
10775 */
10776 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10777 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10778
10779 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10780 $all_modules_user_id_license_ids_map = array();
10781 }
10782
10783 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10784 $all_modules_user_id_license_ids_map[ $module_id ] :
10785 array();
10786
10787 if ( FS_User::is_valid_id( $user_id ) ) {
10788 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10789 $user_id_license_ids_map[ $user_id ] :
10790 array();
10791 }
10792
10793 return $user_id_license_ids_map;
10794 }
10795
10796 /**
10797 * @author Leo Fajardo (@leorw)
10798 * @since 2.0.0
10799 *
10800 * @param array $new_user_id_license_ids_map
10801 * @param number $module_id
10802 * @param number|null $user_id
10803 */
10804 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10805 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10806 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10807 $all_modules_user_id_license_ids_map = array();
10808 }
10809
10810 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10811 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10812 }
10813
10814 if ( FS_User::is_valid_id( $user_id ) ) {
10815 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10816 } else {
10817 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10818 }
10819
10820 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10821 }
10822
10823 /**
10824 * Get a collection of the user's linked license IDs.
10825 *
10826 * @author Vova Feldman (@svovaf)
10827 * @since 2.0.0
10828 *
10829 * @param number $user_id
10830 *
10831 * @return number[]
10832 */
10833 private function get_user_linked_license_ids( $user_id ) {
10834 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10835 }
10836
10837 /**
10838 * Override the user's linked license IDs with a new IDs collection.
10839 *
10840 * @author Vova Feldman (@svovaf)
10841 * @since 2.0.0
10842 *
10843 * @param number $user_id
10844 * @param number[] $license_ids
10845 */
10846 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10847 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10848 }
10849
10850 /**
10851 * Link a specified license ID to a given user.
10852 *
10853 * @author Vova Feldman (@svovaf)
10854 * @since 2.0.0
10855 *
10856 * @param number $license_id
10857 * @param number $user_id
10858 */
10859 private function link_license_2_user( $license_id, $user_id ) {
10860 $license_ids = $this->get_user_linked_license_ids( $user_id );
10861
10862 if ( in_array( $license_id, $license_ids ) ) {
10863 // License already linked.
10864 return;
10865 }
10866
10867 $license_ids[] = $license_id;
10868
10869 $this->set_user_linked_license_ids( $user_id, $license_ids );
10870 }
10871
10872 /**
10873 * @param string|bool $module_type
10874 *
10875 * @return FS_Plugin_Plan[]
10876 */
10877 private static function get_all_plans( $module_type = false ) {
10878 $plans = self::get_account_option( 'plans', $module_type );
10879
10880 if ( ! is_array( $plans ) ) {
10881 $plans = array();
10882 }
10883
10884 return $plans;
10885 }
10886
10887 /**
10888 * @author Vova Feldman (@svovaf)
10889 * @since 1.0.4
10890 *
10891 * @return FS_Plugin_Tag[]
10892 */
10893 private static function get_all_updates() {
10894 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10895
10896 if ( ! is_array( $updates ) ) {
10897 $updates = array();
10898 }
10899
10900 return $updates;
10901 }
10902
10903 /**
10904 * @author Vova Feldman (@svovaf)
10905 * @since 1.0.6
10906 *
10907 * @return array<number,FS_Plugin[]>|false
10908 */
10909 private static function get_all_addons() {
10910 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10911
10912 if ( ! is_array( $addons ) ) {
10913 $addons = array();
10914 }
10915
10916 return $addons;
10917 }
10918
10919 /**
10920 * @author Vova Feldman (@svovaf)
10921 * @since 1.0.6
10922 *
10923 * @return number[]|false
10924 */
10925 private static function get_all_account_addons() {
10926 $addons = self::$_accounts->get_option( 'account_addons', array() );
10927
10928 if ( ! is_array( $addons ) ) {
10929 $addons = array();
10930 }
10931
10932 return $addons;
10933 }
10934
10935 /**
10936 * Check if user has connected his account (opted-in).
10937 *
10938 * Note:
10939 * If the user opted-in and opted-out on a later stage,
10940 * this will still return true. If you want to check if the
10941 * user is currently opted-in, use:
10942 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10943 *
10944 * @author Vova Feldman (@svovaf)
10945 * @since 1.0.1
10946 *
10947 * @param bool $ignore_anonymous_state Since 2.5.1
10948 *
10949 * @return bool
10950 */
10951 function is_registered( $ignore_anonymous_state = false ) {
10952 return (
10953 is_object( $this->_user ) &&
10954 (
10955 $this->is_premium() ||
10956 $ignore_anonymous_state ||
10957 ! $this->is_anonymous()
10958 )
10959 );
10960 }
10961
10962 /**
10963 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10964 *
10965 * @author Leo Fajardo (@leorw)
10966 * @since 1.2.1.5
10967 *
10968 * @return bool
10969 */
10970 function is_tracking_allowed( $blog_id = null, $install = null ) {
10971 if ( is_null( $install ) ) {
10972 $install = is_null( $blog_id ) ?
10973 $this->_site :
10974 $this->get_install_by_blog_id( $blog_id );
10975 }
10976
10977 return (
10978 is_object( $install ) &&
10979 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
10980 );
10981 }
10982
10983 /**
10984 * Returns TRUE if the user never opted-in or manually opted-out.
10985 *
10986 * @author Vova Feldman (@svovaf)
10987 * @since 1.2.1.5
10988 *
10989 * @param int|null $blog_id
10990 *
10991 * @return bool
10992 */
10993 function is_tracking_prohibited( $blog_id = null ) {
10994 return (
10995 ! $this->is_registered( true ) ||
10996 ! $this->is_tracking_allowed( $blog_id )
10997 );
10998 }
10999
11000 /**
11001 * @author Leo Fajardo (@leorw)
11002 * @since 2.4.0
11003 *
11004 * @return bool
11005 */
11006 function is_bundle_license_auto_activation_enabled() {
11007 return $this->is_addon() ?
11008 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
11009 $this->_is_bundle_license_auto_activation_enabled;
11010 }
11011
11012 /**
11013 * @author Vova Feldman (@svovaf)
11014 * @since 1.0.4
11015 *
11016 * @return FS_Plugin
11017 */
11018 function get_plugin() {
11019 return $this->_plugin;
11020 }
11021
11022 /**
11023 * @author Vova Feldman (@svovaf)
11024 * @since 1.0.3
11025 *
11026 * @return FS_User
11027 */
11028 function get_user() {
11029 return $this->_user;
11030 }
11031
11032 /**
11033 * @author Vova Feldman (@svovaf)
11034 * @since 1.0.3
11035 *
11036 * @return FS_Site
11037 */
11038 function get_site() {
11039 return $this->_site;
11040 }
11041
11042 /**
11043 * @author Leo Fajardo (@leorw)
11044 * @since 2.5.0
11045 */
11046 function store_site( $site ) {
11047 $this->_site = $site;
11048 $this->_store_site( true );
11049 }
11050
11051 /**
11052 * 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).
11053 *
11054 * @author Leo Fajardo (@leorw)
11055 * @since 2.5.0
11056 */
11057 function delete_current_install( $back_up ) {
11058 // Back up and delete the unique ID.
11059 if ( $back_up ) {
11060 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
11061 }
11062
11063 self::$_accounts->set_option( 'unique_id', null );
11064
11065 if ( $back_up ) {
11066 // 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).
11067 $this->back_up_site();
11068 }
11069
11070 $this->_delete_site();
11071 $this->_site = null;
11072 }
11073
11074 /**
11075 * @author Leo Fajardo (@leorw)
11076 * @since 2.5.0
11077 */
11078 function restore_backup_site() {
11079 self::$_accounts->set_option(
11080 'unique_id',
11081 self::$_accounts->get_option( 'prev_unique_id' )
11082 );
11083
11084 $sites = self::get_all_sites( $this->_module_type, null, true );
11085 $this->store_site( clone $sites[ $this->_slug ] );
11086 }
11087
11088 /**
11089 * Get plugin add-ons.
11090 *
11091 * @author Vova Feldman (@svovaf)
11092 * @since 1.0.6
11093 *
11094 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
11095 *
11096 * @param bool $flush
11097 *
11098 * @return FS_Plugin[]|false
11099 */
11100 function get_addons( $flush = false ) {
11101 $this->_logger->entrance();
11102
11103 if ( ! $this->_has_addons ) {
11104 return false;
11105 }
11106
11107 $addons = $this->sync_addons( $flush );
11108
11109 return ( ! is_array( $addons ) || empty( $addons ) ) ?
11110 false :
11111 $addons;
11112 }
11113
11114 /**
11115 * @author Vova Feldman (@svovaf)
11116 * @since 1.0.6
11117 *
11118 * @return number[]|false
11119 */
11120 function get_account_addons() {
11121 $this->_logger->entrance();
11122
11123 $addons = self::get_all_account_addons();
11124
11125 if ( ! is_array( $addons ) ||
11126 ! isset( $addons[ $this->_plugin->id ] ) ||
11127 ! is_array( $addons[ $this->_plugin->id ] ) ||
11128 0 === count( $addons[ $this->_plugin->id ] )
11129 ) {
11130 return false;
11131 }
11132
11133 return $addons[ $this->_plugin->id ];
11134 }
11135
11136 /**
11137 * Check if user has any
11138 *
11139 * @author Vova Feldman (@svovaf)
11140 * @since 1.1.6
11141 *
11142 * @return bool
11143 */
11144 function has_account_addons() {
11145 $addons = $this->get_account_addons();
11146
11147 return is_array( $addons ) && ( 0 < count( $addons ) );
11148 }
11149
11150
11151 /**
11152 * Get add-on by ID (from local data).
11153 *
11154 * @author Vova Feldman (@svovaf)
11155 * @since 1.0.6
11156 *
11157 * @param number $id
11158 *
11159 * @return FS_Plugin|false
11160 */
11161 function get_addon( $id ) {
11162 $this->_logger->entrance();
11163
11164 $addons = $this->get_addons();
11165
11166 if ( is_array( $addons ) ) {
11167 foreach ( $addons as $addon ) {
11168 if ( $id == $addon->id ) {
11169 return $addon;
11170 }
11171 }
11172 }
11173
11174 return false;
11175 }
11176
11177 /**
11178 * Get add-on by slug (from local data).
11179 *
11180 * @author Vova Feldman (@svovaf)
11181 * @since 1.0.6
11182 *
11183 * @param string $slug
11184 *
11185 * @param bool $flush
11186 *
11187 * @return FS_Plugin|false
11188 */
11189 function get_addon_by_slug( $slug, $flush = false ) {
11190 $this->_logger->entrance();
11191
11192 $addons = $this->get_addons( $flush );
11193
11194 if ( is_array( $addons ) ) {
11195 foreach ( $addons as $addon ) {
11196 if ( $slug === $addon->slug ) {
11197 return $addon;
11198 }
11199 }
11200 }
11201
11202 return false;
11203 }
11204
11205 /**
11206 * @var array<number,object[]> {
11207 * @key number Add-on ID.
11208 * @val object[] The add-on's plans and prices object.
11209 * }
11210 */
11211 private $plans_and_pricing_by_addon_id;
11212
11213 /**
11214 * @author Leo Fajardo (@leorw)
11215 * @since 2.3.0
11216 *
11217 * @return array<number,object[]> {
11218 * @key number Add-on ID.
11219 * @val object[] The add-on's plans and prices object.
11220 * }
11221 */
11222 function _get_addons_plans_and_pricing_map_by_id() {
11223 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11224 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11225
11226 $plans_and_pricing_by_addon_id = array();
11227 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11228 foreach ( $result->addons as $addon ) {
11229 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11230 }
11231 }
11232
11233 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11234 }
11235
11236 return $this->plans_and_pricing_by_addon_id;
11237 }
11238
11239 /**
11240 * @author Leo Fajardo (@leorw)
11241 * @since 2.3.0
11242 *
11243 * @param number $addon_id
11244 * @param bool $is_installed
11245 *
11246 * @return array
11247 */
11248 function _get_addon_info( $addon_id, $is_installed ) {
11249 $addon = $this->get_addon( $addon_id );
11250
11251 if ( ! is_object( $addon ) ) {
11252 // Unexpected call.
11253 return array();
11254 }
11255
11256 $slug = $addon->slug;
11257
11258 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11259
11260 if ( ! fs_is_network_admin() ) {
11261 // Get blog-level activated installations.
11262 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11263 } else {
11264 $sites = null;
11265
11266 if ( $this->is_addon_activated( $addon_id ) &&
11267 $this->get_addon_instance( $addon_id )->is_network_active()
11268 ) {
11269 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11270 // Get network-level activated installations.
11271 $sites = self::maybe_get_entities_account_option(
11272 'sites',
11273 array(),
11274 $addon_storage->network_install_blog_id
11275 );
11276 }
11277 }
11278 }
11279
11280 $addon_info = array(
11281 'is_connected' => false,
11282 'slug' => $slug,
11283 'title' => $addon->title,
11284 'is_whitelabeled' => $addon_storage->is_whitelabeled
11285 );
11286
11287 if ( ! $is_installed ) {
11288 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11289
11290 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11291 $has_paid_plan = false;
11292 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11293
11294 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11295 foreach ( $plans as $plan ) {
11296 if ( isset( $plan->pricing ) &&
11297 is_array( $plan->pricing ) &&
11298 count( $plan->pricing ) > 0
11299 ) {
11300 $has_paid_plan = true;
11301 break;
11302 }
11303 }
11304 }
11305
11306 $addon_info['has_paid_plan'] = $has_paid_plan;
11307 }
11308 }
11309
11310 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11311 return $addon_info;
11312 }
11313
11314 $site = $sites[ $slug ];
11315
11316 $addon_info['is_connected'] = (
11317 ( $addon->parent_plugin_id == $this->get_id() ) &&
11318 is_object( $site ) &&
11319 FS_Site::is_valid_id( $site->id ) &&
11320 FS_User::is_valid_id( $site->user_id ) &&
11321 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11322 );
11323
11324 if ( $addon_info['is_connected'] && $is_installed ) {
11325 return $addon_info;
11326 }
11327
11328 $addon_info['site'] = $site;
11329
11330 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11331 if ( isset( $plugins_data[ $slug ] ) ) {
11332 $plugin_data = $plugins_data[ $slug ];
11333
11334 $addon_info['version'] = $plugin_data->version;
11335 }
11336
11337 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11338 if ( isset( $all_plans[ $slug ] ) ) {
11339 $plans = $all_plans[ $slug ];
11340
11341 foreach ( $plans as $plan ) {
11342 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11343 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11344 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11345 break;
11346 }
11347 }
11348 }
11349
11350 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11351 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11352 foreach ( $licenses[ $addon_id ] as $license ) {
11353 if ( $license->id == $site->license_id ) {
11354 $addon_info['license'] = $license;
11355 break;
11356 }
11357 }
11358 }
11359
11360 if ( isset( $addon_info['license'] ) ) {
11361 if ( isset( $addon_storage->subscriptions ) &&
11362 ! empty( $addon_storage->subscriptions )
11363 ) {
11364 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11365
11366 foreach ( $addon_subscriptions as $subscription ) {
11367 if ( $subscription->license_id == $site->license_id ) {
11368 $addon_info['subscription'] = $subscription;
11369 break;
11370 }
11371 }
11372 }
11373 }
11374
11375 return $addon_info;
11376 }
11377
11378 /**
11379 * @author Vova Feldman (@svovaf)
11380 * @since 2.0.0
11381 *
11382 * @param number $user_id
11383 *
11384 * @return FS_User
11385 */
11386 static function _get_user_by_id( $user_id ) {
11387 self::$_static_logger->entrance( "user_id = {$user_id}" );
11388
11389 $users = self::get_all_users();
11390
11391 if ( is_array( $users ) ) {
11392 if ( isset( $users[ $user_id ] ) &&
11393 $users[ $user_id ] instanceof FS_User &&
11394 $user_id == $users[ $user_id ]->id
11395 ) {
11396 return $users[ $user_id ];
11397 }
11398
11399 // If user wasn't found by the key, iterate over all the users collection.
11400 foreach ( $users as $user ) {
11401 /**
11402 * @var FS_User $user
11403 */
11404 if ( $user_id == $user->id ) {
11405 return $user;
11406 }
11407 }
11408 }
11409
11410 return null;
11411 }
11412
11413 /**
11414 * Checks if a Freemius user_id is associated with a super-admin.
11415 *
11416 * @author Vova Feldman (@svovaf)
11417 * @since 2.0.0
11418 *
11419 * @param number $user_id
11420 *
11421 * @return bool
11422 */
11423 private static function is_super_admin( $user_id ) {
11424 $is_super_admin = false;
11425
11426 $user = self::_get_user_by_id( $user_id );
11427
11428 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11429 self::require_pluggable_essentials();
11430
11431 $wp_user = get_user_by( 'email', $user->email );
11432
11433 if ( $wp_user instanceof WP_User ) {
11434 $super_admins = get_super_admins();
11435 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11436 }
11437 }
11438
11439 return $is_super_admin;
11440 }
11441
11442 #----------------------------------------------------------------------------------
11443 #region Plans & Licensing
11444 #----------------------------------------------------------------------------------
11445
11446 /**
11447 * Check if running premium plugin code.
11448 *
11449 * @author Vova Feldman (@svovaf)
11450 * @since 1.0.5
11451 *
11452 * @return bool
11453 */
11454 function is_premium() {
11455 /**
11456 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11457 * `register_constructor_hooks` method.
11458 *
11459 * @author Leo Fajardo (@leorw)
11460 * @since 2.2.3
11461 */
11462 return is_object( $this->_plugin ) ?
11463 $this->_plugin->is_premium :
11464 false;
11465 }
11466
11467 /**
11468 * Get site's plan ID.
11469 *
11470 * @author Vova Feldman (@svovaf)
11471 * @since 1.0.2
11472 *
11473 * @return number
11474 */
11475 function get_plan_id() {
11476 return $this->_site->plan_id;
11477 }
11478
11479 /**
11480 * Get site's plan title.
11481 *
11482 * @author Vova Feldman (@svovaf)
11483 * @since 1.0.2
11484 *
11485 * @return string
11486 */
11487 function get_plan_title() {
11488 $plan = $this->get_plan();
11489
11490 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11491 }
11492
11493 /**
11494 * Get site's plan name.
11495 *
11496 * @author Vova Feldman (@svovaf)
11497 * @since 2.0.0
11498 *
11499 * @return string
11500 */
11501 function get_plan_name() {
11502 $plan = $this->get_plan();
11503
11504 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11505 }
11506
11507 /**
11508 * @author Vova Feldman (@svovaf)
11509 * @since 1.0.9
11510 *
11511 * @return FS_Plugin_Plan|false
11512 */
11513 function get_plan() {
11514 if ( ! is_object( $this->_site ) ) {
11515 return false;
11516 }
11517
11518 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11519 $this->_get_plan_by_id( $this->_site->plan_id ) :
11520 false;
11521 }
11522
11523 /**
11524 * @author Vova Feldman (@svovaf)
11525 * @since 1.0.3
11526 *
11527 * @return bool
11528 */
11529 function is_trial() {
11530 $this->_logger->entrance();
11531
11532 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11533 return false;
11534 }
11535
11536 return $this->_site->is_trial();
11537 }
11538
11539 /**
11540 * Check if currently in a trial with payment method (credit card or paypal).
11541 *
11542 * @author Vova Feldman (@svovaf)
11543 * @since 1.1.7
11544 *
11545 * @return bool
11546 */
11547 function is_paid_trial() {
11548 $this->_logger->entrance();
11549
11550 if ( ! $this->is_trial() ) {
11551 return false;
11552 }
11553
11554 if ( ! $this->has_active_valid_license() ) {
11555 return false;
11556 }
11557
11558 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11559 return false;
11560 }
11561
11562 /**
11563 * @var FS_Subscription $subscription
11564 */
11565 $subscription = $this->_get_subscription( $this->_license->id );
11566
11567 return ( is_object( $subscription ) && $subscription->is_active() );
11568 }
11569
11570 /**
11571 * Check if trial already utilized.
11572 *
11573 * @since 1.0.9
11574 *
11575 * @return bool
11576 */
11577 function is_trial_utilized() {
11578 $this->_logger->entrance();
11579
11580 if ( ! $this->is_registered() ) {
11581 return false;
11582 }
11583
11584 return $this->_site->is_trial_utilized();
11585 }
11586
11587 /**
11588 * Get trial plan information (if in trial).
11589 *
11590 * @author Vova Feldman (@svovaf)
11591 * @since 1.0.9
11592 *
11593 * @return bool|FS_Plugin_Plan
11594 */
11595 function get_trial_plan() {
11596 $this->_logger->entrance();
11597
11598 if ( ! $this->is_trial() ) {
11599 return false;
11600 }
11601
11602 // Try to load plan from local cache.
11603 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11604
11605 if ( ! is_object( $trial_plan ) ) {
11606 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11607
11608 /**
11609 * If managed to fetch the plan, add it to the plans collection.
11610 */
11611 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11612 if ( ! is_array( $this->_plans ) ) {
11613 $this->_plans = array();
11614 }
11615
11616 $this->_plans[] = $trial_plan;
11617 $this->_store_plans();
11618 }
11619 }
11620
11621 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11622 return $trial_plan;
11623 }
11624
11625 /**
11626 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11627 */
11628 $trial_plan = new FS_Plugin_Plan();
11629 $trial_plan->id = $this->_site->trial_plan_id;
11630 $trial_plan->name = 'pro';
11631 $trial_plan->title = 'Pro';
11632
11633 return $trial_plan;
11634 }
11635
11636 /**
11637 * Check if the user has an activate, non-expired license on current plugin's install.
11638 *
11639 * @since 1.0.9
11640 *
11641 * @return bool
11642 */
11643 function is_paying() {
11644 $this->_logger->entrance();
11645
11646 if ( ! $this->is_registered( true ) ) {
11647 return false;
11648 }
11649
11650 if ( ! $this->has_paid_plan() ) {
11651 return false;
11652 }
11653
11654 return (
11655 ! $this->is_trial() &&
11656 'free' !== $this->get_plan_name() &&
11657 $this->has_active_valid_license()
11658 );
11659 }
11660
11661 /**
11662 * @author Vova Feldman (@svovaf)
11663 * @since 1.0.4
11664 *
11665 * @return bool
11666 */
11667 function is_free_plan() {
11668 if ( ! $this->is_registered() ) {
11669 return true;
11670 }
11671
11672 if ( ! $this->has_paid_plan() ) {
11673 return true;
11674 }
11675
11676 return (
11677 'free' === $this->get_plan_name() ||
11678 ! $this->has_features_enabled_license()
11679 );
11680 }
11681
11682 /**
11683 * @author Vova Feldman (@svovaf)
11684 * @since 1.0.5
11685 *
11686 * @return bool
11687 */
11688 function _has_premium_license() {
11689 $this->_logger->entrance();
11690
11691 $premium_license = $this->_get_available_premium_license();
11692
11693 return ( false !== $premium_license );
11694 }
11695
11696 /**
11697 * Check if user has any licenses associated with the plugin (including expired or blocking).
11698 *
11699 * @author Vova Feldman (@svovaf)
11700 * @since 1.1.7.3
11701 *
11702 * @param bool $including_foreign
11703 *
11704 * @return bool
11705 */
11706 function has_any_license( $including_foreign = true ) {
11707 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11708 return false;
11709 }
11710
11711 if ( $including_foreign ) {
11712 return true;
11713 }
11714
11715 foreach ( $this->_licenses as $license ) {
11716 if ( $this->_user->id == $license->user_id ) {
11717 return true;
11718 }
11719 }
11720
11721 return false;
11722 }
11723
11724 /**
11725 * @author Vova Feldman (@svovaf)
11726 * @since 1.0.5
11727 *
11728 * @param bool|null $is_localhost
11729 *
11730 * @return FS_Plugin_License|false
11731 */
11732 function _get_available_premium_license( $is_localhost = null ) {
11733 $this->_logger->entrance();
11734
11735 $licenses = $this->get_available_premium_licenses( $is_localhost );
11736 if ( ! empty( $licenses ) ) {
11737 return $licenses[0];
11738 }
11739
11740 return false;
11741 }
11742
11743 /**
11744 * @author Vova Feldman (@svovaf)
11745 * @since 1.0.5
11746 *
11747 * @param bool|null $is_localhost
11748 *
11749 * @return FS_Plugin_License[]
11750 */
11751 function get_available_premium_licenses( $is_localhost = null ) {
11752 $this->_logger->entrance();
11753
11754 $licenses = array();
11755 if ( ! $this->has_paid_plan() ) {
11756 return $licenses;
11757 }
11758
11759 if ( is_array( $this->_licenses ) ) {
11760 foreach ( $this->_licenses as $license ) {
11761 if ( ! $license->can_activate( $is_localhost ) ) {
11762 continue;
11763 }
11764
11765 $licenses[] = $license;
11766 }
11767 }
11768
11769 return $licenses;
11770 }
11771
11772 /**
11773 * Sync local plugin plans with remote server.
11774 *
11775 * 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).
11776 *
11777 * @author Vova Feldman (@svovaf)
11778 * @since 1.0.5
11779 *
11780 * @return FS_Plugin_Plan[]|object
11781 */
11782 function _sync_plans() {
11783 $plans = $this->_fetch_plugin_plans();
11784
11785 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11786 $plans_map = array();
11787 foreach ( $plans as $plan ) {
11788 $plans_map[ $plan->id ] = true;
11789 }
11790
11791 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11792
11793 foreach ( $plans_ids_to_keep as $plan_id ) {
11794 if ( isset( $plans_map[ $plan_id ] ) ) {
11795 continue;
11796 }
11797
11798 $missing_plan = self::_get_plan_by_id( $plan_id );
11799
11800 if ( is_object( $missing_plan ) ) {
11801 $plans[] = $missing_plan;
11802 }
11803 }
11804
11805 $this->_plans = $plans;
11806 $this->_store_plans();
11807 }
11808
11809 $this->do_action( 'after_plans_sync', $plans );
11810
11811 return $this->_plans;
11812 }
11813
11814 /**
11815 * Check if specified plan exists locally. If not, fetch it and store it.
11816 *
11817 * @author Vova Feldman (@svovaf)
11818 * @since 2.0.0
11819 *
11820 * @param number $plan_id
11821 *
11822 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11823 */
11824 private function sync_plan_if_not_exist( $plan_id ) {
11825 $plan = self::_get_plan_by_id( $plan_id );
11826
11827 if ( is_object( $plan ) ) {
11828 // Plan already exists.
11829 return $plan;
11830 }
11831
11832 $plan = $this->fetch_plan_by_id( $plan_id );
11833
11834 if ( $plan instanceof FS_Plugin_Plan ) {
11835 $this->_plans[] = $plan;
11836 $this->_store_plans();
11837
11838 return $plan;
11839 }
11840
11841 return $plan;
11842 }
11843
11844 /**
11845 * Check if specified license exists locally. If not, fetch it and store it.
11846 *
11847 * @author Vova Feldman (@svovaf)
11848 * @since 2.0.0
11849 *
11850 * @param number $license_id
11851 * @param string $license_key
11852 *
11853 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11854 */
11855 private function sync_license_if_not_exist( $license_id, $license_key ) {
11856 $license = $this->_get_license_by_id( $license_id );
11857
11858 if ( is_object( $license ) ) {
11859 // License already exists.
11860 return $license;
11861 }
11862
11863 $license = $this->fetch_license_by_key( $license_id, $license_key );
11864
11865 if ( $license instanceof FS_Plugin_License ) {
11866 $this->_licenses[] = $license;
11867
11868 $this->set_license( $license );
11869
11870 $this->_store_licenses();
11871
11872 return $license;
11873 }
11874
11875 return $license;
11876 }
11877
11878 /**
11879 * Get a collection of unique plan IDs that are associated with any installs in the network.
11880 *
11881 * @author Leo Fajardo (@leorw)
11882 * @since 2.0.0
11883 *
11884 * @return number[]
11885 */
11886 private function get_plans_ids_associated_with_installs() {
11887 if ( ! is_multisite() ) {
11888 if ( ! is_object( $this->_site ) ||
11889 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11890 ) {
11891 return array();
11892 }
11893
11894 return array( $this->_site->plan_id );
11895 }
11896
11897 $plan_ids = array();
11898 $sites = self::get_sites();
11899 foreach ( $sites as $site ) {
11900 $blog_id = self::get_site_blog_id( $site );
11901 $install = $this->get_install_by_blog_id( $blog_id );
11902
11903 if ( ! is_object( $install ) ||
11904 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11905 ) {
11906 continue;
11907 }
11908
11909 $plan_ids[ $install->plan_id ] = true;
11910 }
11911
11912 return array_keys( $plan_ids );
11913 }
11914
11915 /**
11916 * Get a collection of unique license IDs that are associated with any installs in the network.
11917 *
11918 * @author Leo Fajardo (@leorw)
11919 * @since 2.0.0
11920 *
11921 * @return number[]
11922 */
11923 private function get_license_ids_associated_with_installs() {
11924 if ( ! $this->_is_network_active ) {
11925 if ( ! is_object( $this->_site ) ||
11926 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11927 ) {
11928 return array();
11929 }
11930
11931 return array( $this->_site->license_id );
11932 }
11933
11934 $license_ids = array();
11935 $sites = self::get_sites();
11936 foreach ( $sites as $site ) {
11937 $blog_id = self::get_site_blog_id( $site );
11938 $install = $this->get_install_by_blog_id( $blog_id );
11939
11940 if ( ! is_object( $install ) ||
11941 ! FS_Plugin_License::is_valid_id( $install->license_id )
11942 ) {
11943 continue;
11944 }
11945
11946 $license_ids[ $install->license_id ] = true;
11947 }
11948
11949 return array_keys( $license_ids );
11950 }
11951
11952 /**
11953 * @author Vova Feldman (@svovaf)
11954 * @since 1.0.5
11955 *
11956 * @param number $id
11957 *
11958 * @return FS_Plugin_Plan|false
11959 */
11960 function _get_plan_by_id( $id ) {
11961 $this->_logger->entrance();
11962
11963 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11964 $this->_sync_plans();
11965 }
11966
11967 foreach ( $this->_plans as $plan ) {
11968 if ( $id == $plan->id ) {
11969 return $plan;
11970 }
11971 }
11972
11973 return false;
11974 }
11975
11976 /**
11977 * @author Vova Feldman (@svovaf)
11978 * @since 1.1.8.1
11979 *
11980 * @param string $name
11981 *
11982 * @return FS_Plugin_Plan|false
11983 */
11984 private function get_plan_by_name( $name ) {
11985 $this->_logger->entrance();
11986
11987 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11988 $this->_sync_plans();
11989 }
11990
11991 foreach ( $this->_plans as $plan ) {
11992 if ( $name == $plan->name ) {
11993 return $plan;
11994 }
11995 }
11996
11997 return false;
11998 }
11999
12000 /**
12001 * Sync local licenses with remote server.
12002 *
12003 * @author Vova Feldman (@svovaf)
12004 * @since 1.0.6
12005 *
12006 * @param number|bool $site_license_id
12007 * @param number|null $blog_id
12008 *
12009 * @return FS_Plugin_License[]|object
12010 */
12011 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
12012 $this->_logger->entrance();
12013
12014 $is_network_admin = fs_is_network_admin();
12015
12016 if ( $is_network_admin && is_null( $blog_id ) ) {
12017 $all_licenses = self::get_all_licenses( $this->_module_id );
12018 } else {
12019 $all_licenses = $this->get_user_licenses( $this->_user->id );
12020 }
12021
12022 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
12023
12024 $all_licenses_map = array();
12025 foreach ( $all_licenses as $license ) {
12026 $all_licenses_map[ $license->id ] = true;
12027 }
12028
12029 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
12030
12031 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
12032 $licenses_map = array();
12033 foreach ( $licenses as $license ) {
12034 $licenses_map[ $license->id ] = true;
12035 }
12036
12037 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
12038 // foreach ( $license_ids_to_keep as $license_id ) {
12039 // if ( isset( $licenses_map[ $license_id ] ) ) {
12040 // continue;
12041 // }
12042 //
12043 // $missing_license = self::_get_license_by_id( $license_id, false );
12044 // if ( is_object( $missing_license ) ) {
12045 // $licenses[] = $missing_license;
12046 // $licenses_map[ $missing_license->id ] = true;
12047 // }
12048 // }
12049
12050 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
12051
12052 foreach ( $user_license_ids as $key => $license_id ) {
12053 if ( ! isset( $licenses_map[ $license_id ] ) ) {
12054 // Remove access to licenses that no longer exist.
12055 unset( $user_license_ids[ $key ] );
12056 }
12057 }
12058
12059 if ( ! empty( $user_license_ids ) ) {
12060 foreach ( $licenses_map as $license_id => $value ) {
12061 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
12062 // Associate new licenses with the user who triggered the license syncing.
12063 $user_license_ids[] = $license_id;
12064 }
12065 }
12066
12067 $user_license_ids = array_unique( $user_license_ids );
12068 } else {
12069 $user_license_ids = array_keys( $licenses_map );
12070 }
12071
12072 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
12073 $user_licenses = array();
12074 foreach ( $licenses as $license ) {
12075 if ( ! in_array( $license->id, $user_license_ids ) ) {
12076 continue;
12077 }
12078
12079 $user_licenses[] = $license;
12080 }
12081
12082 $this->_licenses = $user_licenses;
12083 } else {
12084 $this->_licenses = $licenses;
12085 }
12086
12087 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
12088
12089 $this->_store_licenses( true, $this->_module_id, $licenses );
12090 }
12091
12092 // Update current license.
12093 if ( is_object( $this->_license ) ) {
12094 $license = $this->_get_license_by_id( $this->_license->id );
12095
12096 if ( is_object( $license ) ) {
12097 /**
12098 * `$license` can be `false` in case a user change action has just been completed and this method
12099 * has synced the `$this->_licenses` collection for the new user. In this case, the
12100 * `$this->_licenses` collection may have only the newly activated license that is associated with
12101 * the new user. `set_license` will eventually be called in the same request by the logic that
12102 * follows outside this method which will detect that the install's license has been updated, and
12103 * then `_update_site_license` will be called which in turn will call `set_license`.
12104 *
12105 * @author Leo Fajardo (@leorw)
12106 * @since 2.3.2
12107 */
12108 $this->set_license( $license );
12109 }
12110 }
12111
12112 return $this->_licenses;
12113 }
12114
12115 /**
12116 * @author Vova Feldman (@svovaf)
12117 * @since 1.0.5
12118 *
12119 * @param number $id
12120 * @param bool $sync_licenses
12121 *
12122 * @return FS_Plugin_License|false
12123 */
12124 function _get_license_by_id( $id, $sync_licenses = true ) {
12125 $this->_logger->entrance();
12126
12127 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
12128 return false;
12129 }
12130
12131 /**
12132 * When running from the network level admin and opted-in from the network,
12133 * check if the license exists in the network user licenses collection.
12134 *
12135 * @author Vova Feldman (@svovaf)
12136 * @since 2.0.0
12137 */
12138 if ( fs_is_network_admin() &&
12139 $this->is_network_registered() &&
12140 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
12141 ) {
12142 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
12143
12144 foreach ( $licenses as $license ) {
12145 if ( $id == $license->id ) {
12146 return $license;
12147 }
12148 }
12149 }
12150
12151 if ( ! $this->has_any_license() && $sync_licenses ) {
12152 $this->_sync_licenses( $id );
12153 }
12154
12155 if ( is_array( $this->_licenses ) ) {
12156 foreach ( $this->_licenses as $license ) {
12157 if ( $id == $license->id ) {
12158 return $license;
12159 }
12160 }
12161 }
12162
12163 return false;
12164 }
12165
12166 /**
12167 * 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.
12168 *
12169 * @author Vova Feldman (@svovaf)
12170 * @since 2.0.0
12171 *
12172 * @param number $id
12173 *
12174 * @return FS_Plugin_License
12175 */
12176 private function get_license_by_id( $id ) {
12177 $licenses = self::get_all_licenses( $this->_module_id );
12178
12179 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
12180 foreach ( $licenses as $license ) {
12181 if ( $id == $license->id ) {
12182 return $license;
12183 }
12184 }
12185 }
12186
12187 return null;
12188 }
12189
12190 /**
12191 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
12192 *
12193 * @author Vova Feldman (@svovaf)
12194 * @since 2.0.0
12195 *
12196 * @return \FS_Plugin_License|mixed
12197 */
12198 private function sync_site_license() {
12199 $api = $this->get_api_user_scope();
12200
12201 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
12202
12203 if ( ! $this->is_api_result_entity( $result ) ) {
12204 return $result;
12205 }
12206
12207 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12208 $this->_store_licenses();
12209
12210 return $license;
12211 }
12212
12213 /**
12214 * Get all user's available licenses for the current module.
12215 *
12216 * @author Vova Feldman (@svovaf)
12217 * @since 2.0.0
12218 *
12219 * @param number $user_id
12220 *
12221 * @return FS_Plugin_License[]
12222 */
12223 private function get_user_licenses( $user_id ) {
12224 $all_licenses = self::get_all_licenses( $this->_module_id );
12225 if ( empty( $all_licenses ) ) {
12226 return array();
12227 }
12228
12229 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12230 if ( empty( $user_license_ids ) ) {
12231 return array();
12232 }
12233
12234 $licenses = array();
12235 foreach ( $all_licenses as $license ) {
12236 if ( in_array( $license->id, $user_license_ids ) ) {
12237 $licenses[] = $license;
12238 }
12239 }
12240
12241 return $licenses;
12242 }
12243
12244 /**
12245 * Checks if the context license is network activated except on the given blog ID.
12246 *
12247 * @author Vova Feldman (@svovaf)
12248 * @since 2.0.0
12249 *
12250 * @param int $except_blog_id
12251 *
12252 * @return bool
12253 */
12254 private function is_license_network_active( $except_blog_id = 0 ) {
12255 $this->_logger->entrance();
12256
12257 if ( ! is_object( $this->_license ) ) {
12258 return false;
12259 }
12260
12261 $sites = self::get_sites();
12262
12263 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12264 // There are more sites than the number of activations, so license cannot be network activated.
12265 return false;
12266 }
12267
12268 foreach ( $sites as $site ) {
12269 $blog_id = self::get_site_blog_id( $site );
12270
12271 if ( $except_blog_id == $blog_id ) {
12272 // Skip excluded blog.
12273 continue;
12274 }
12275
12276 $install = $this->get_install_by_blog_id( $blog_id );
12277
12278 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12279 return false;
12280 }
12281 }
12282
12283 return true;
12284 }
12285
12286 /**
12287 * 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.
12288 *
12289 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12290 *
12291 * @author Vova Feldman (@svovaf)
12292 * @since 2.0.0
12293 *
12294 * @param \FS_User $user
12295 * @param \FS_Plugin_License $license
12296 *
12297 * @return bool
12298 */
12299 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12300 $this->_logger->entrance();
12301
12302 $result = $this->can_activate_license_on_network( $license );
12303
12304 if ( false === $result ) {
12305 return false;
12306 }
12307
12308 $installs_without_license = $result['installs'];
12309 if ( ! empty( $installs_without_license ) ) {
12310 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12311 }
12312
12313 $disconnected_site_ids = $result['sites'];
12314 if ( ! empty( $disconnected_site_ids ) ) {
12315 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12316 }
12317
12318 $this->link_license_2_user( $license->id, $user->id );
12319
12320 // Sync license after activations.
12321 $license->activated += $result['production_count'];
12322 $license->activated_local += $result['localhost_count'];
12323
12324 // $this->_store_licenses()
12325
12326 return true;
12327 }
12328
12329 /**
12330 * Checks if the given license can be activated on the whole network.
12331 *
12332 * @author Vova Feldman (@svovaf)
12333 * @since 2.0.0
12334 *
12335 * @param \FS_Plugin_License $license
12336 *
12337 * @return false|array {
12338 * @type array[int]FS_Site $installs Blog ID to install map.
12339 * @type int[] $sites Non-connected blog IDs.
12340 * @type int $production_count Production sites count.
12341 * @type int $localhost_count Production sites count.
12342 * }
12343 */
12344 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12345 $sites = self::get_sites();
12346
12347 $production_count = 0;
12348 $localhost_count = 0;
12349
12350 $installs_without_license = array();
12351 $disconnected_site_ids = array();
12352
12353 foreach ( $sites as $site ) {
12354 $blog_id = self::get_site_blog_id( $site );
12355 $install = $this->get_install_by_blog_id( $blog_id );
12356
12357 if ( is_object( $install ) ) {
12358 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12359 // License already activated on the install.
12360 continue;
12361 }
12362
12363 $url = $install->url;
12364
12365 $installs_without_license[ $blog_id ] = $install;
12366 } else {
12367 $url = is_object( $site ) ?
12368 $site->siteurl :
12369 self::get_unfiltered_site_url( $blog_id );
12370
12371 $disconnected_site_ids[] = $blog_id;
12372 }
12373
12374 if ( FS_Site::is_localhost_by_address( $url ) ) {
12375 $localhost_count ++;
12376 } else {
12377 $production_count ++;
12378 }
12379 }
12380
12381 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12382 return false;
12383 }
12384
12385 return array(
12386 'installs' => $installs_without_license,
12387 'sites' => $disconnected_site_ids,
12388 'production_count' => $production_count,
12389 'localhost_count' => $localhost_count,
12390 );
12391 }
12392
12393 /**
12394 * Activate a given license on a collection of installs.
12395 *
12396 * @author Vova Feldman (@svovaf)
12397 * @since 2.0.0
12398 *
12399 * @param \FS_User $user
12400 * @param string $license_key
12401 * @param array $blog_2_install_map {
12402 * @key int Blog ID.
12403 * @value FS_Site Blog's associated install.
12404 * }
12405 *
12406 * @return mixed|true
12407 */
12408 private function activate_license_on_many_installs(
12409 FS_User $user,
12410 $license_key,
12411 array $blog_2_install_map
12412 ) {
12413 $params = array(
12414 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12415 );
12416
12417 $install_2_blog_map = array();
12418 foreach ( $blog_2_install_map as $blog_id => $install ) {
12419 $params[] = array( 'id' => $install->id );
12420
12421 $install_2_blog_map[ $install->id ] = $blog_id;
12422 }
12423
12424 $result = $this->get_api_user_scope_by_user( $user )->call(
12425 "plugins/{$this->_plugin->id}/installs.json",
12426 'PUT',
12427 $params
12428 );
12429
12430 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12431 return $result;
12432 }
12433
12434 foreach ( $result->installs as $r_install ) {
12435 $install = new FS_Site( $r_install );
12436 $install->is_disconnected = false;
12437
12438 // Update install.
12439 $this->_store_site(
12440 true,
12441 $install_2_blog_map[ $r_install->id ],
12442 $install
12443 );
12444 }
12445
12446 return true;
12447 }
12448
12449 /**
12450 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12451 *
12452 * @author Vova Feldman (@svovaf)
12453 * @since 2.3.1
12454 *
12455 * @param \FS_User $user
12456 * @param string $license_key
12457 *
12458 * @return true|mixed True if successful, otherwise, the API result.
12459 */
12460 private function activate_license_on_site( FS_User $user, $license_key ) {
12461 return $this->activate_license_on_many_sites( $user, $license_key );
12462 }
12463
12464 /**
12465 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12466 *
12467 * @author Vova Feldman (@svovaf)
12468 * @since 2.0.0
12469 *
12470 * @param \FS_User $user
12471 * @param string $license_key
12472 * @param int[] $site_ids
12473 *
12474 * @return true|mixed True if successful, otherwise, the API result.
12475 */
12476 private function activate_license_on_many_sites(
12477 FS_User $user,
12478 $license_key,
12479 array $site_ids = array()
12480 ) {
12481 $sites = array();
12482 foreach ( $site_ids as $site_id ) {
12483 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12484 }
12485
12486 // Install the plugin.
12487 $result = $this->create_installs_with_user(
12488 $user,
12489 $license_key,
12490 false,
12491 $sites,
12492 false,
12493 true
12494 );
12495
12496 if ( ! $this->is_api_result_entity( $result ) &&
12497 ! $this->is_api_result_object( $result, 'installs' )
12498 ) {
12499 return $result;
12500 }
12501
12502 $installs = array();
12503
12504 if ( $this->is_api_result_entity( $result ) ) {
12505 $install = new FS_Site( $result );
12506
12507 $this->_user = $user;
12508
12509 $this->_store_site( true, null, $install );
12510
12511 $this->_site = $install;
12512
12513 $this->reset_anonymous_mode();
12514 } else {
12515 foreach ( $result->installs as $install ) {
12516 $installs[] = new FS_Site( $install );
12517 }
12518
12519 // Map site addresses to their blog IDs.
12520 $address_to_blog_map = $this->get_address_to_blog_map();
12521
12522 $first_blog_id = null;
12523
12524 foreach ( $installs as $install ) {
12525 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12526 $blog_id = $address_to_blog_map[ $address ];
12527
12528 $this->_store_site( true, $blog_id, $install );
12529
12530 $this->reset_anonymous_mode( $blog_id );
12531
12532 if ( is_null( $first_blog_id ) ) {
12533 $first_blog_id = $blog_id;
12534 }
12535 }
12536
12537 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12538 $this->_storage->network_install_blog_id = $first_blog_id;
12539 }
12540 }
12541
12542 return true;
12543 }
12544
12545 /**
12546 * Sync site's license with user licenses.
12547 *
12548 * @author Vova Feldman (@svovaf)
12549 * @since 1.0.6
12550 *
12551 * @param FS_Plugin_License|null $new_license
12552 *
12553 * @return FS_Plugin_License|null
12554 */
12555 function _update_site_license( $new_license ) {
12556 $this->_logger->entrance();
12557
12558 /**
12559 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12560 * accordingly so that it will also handle the case when an ownership change is done via license
12561 * activation.
12562 *
12563 * @author Leo Fajardo (@leorw)
12564 * @since 2.3.2
12565 */
12566 $this->set_license( $new_license );
12567
12568 if ( ! is_object( $new_license ) ) {
12569 $this->_site->license_id = null;
12570 $this->_sync_site_subscription( null );
12571
12572 return $this->_license;
12573 }
12574
12575 $this->_site->license_id = $this->_license->id;
12576
12577 if ( ! is_array( $this->_licenses ) ) {
12578 $this->_licenses = array();
12579 }
12580
12581 $is_license_found = false;
12582 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12583 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12584 $this->_licenses[ $i ] = $new_license;
12585
12586 $is_license_found = true;
12587 break;
12588 }
12589 }
12590
12591 // If new license just append.
12592 if ( ! $is_license_found ) {
12593 $this->_licenses[] = $new_license;
12594 }
12595
12596 $this->_sync_site_subscription( $new_license );
12597
12598 return $this->_license;
12599 }
12600
12601 /**
12602 * @author Vova Feldman (@svovaf)
12603 * @since 2.3.1
12604 *
12605 * @param \FS_Plugin_License $license
12606 */
12607 private function set_license( FS_Plugin_License $license = null ) {
12608 $this->_license = $license;
12609
12610 $this->maybe_update_whitelabel_flag( $license );
12611 }
12612
12613 /**
12614 * @author Leo Fajardo (@leorw)
12615 * @since 2.3.1
12616 *
12617 * @param FS_Plugin_License $license
12618 */
12619 private function maybe_update_whitelabel_flag( $license ) {
12620 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12621 $this->_storage->is_whitelabeled :
12622 false;
12623
12624 if ( is_object( $license ) ) {
12625 $license_user = self::_get_user_by_id( $license->user_id );
12626
12627 if ( ! is_object( $license_user ) ) {
12628 // If foreign license, do not update the `is_whitelabeled` flag.
12629 return;
12630 }
12631
12632 if ( $this->is_addon() ) {
12633 /**
12634 * Store the last license data to the parent's storage since it's needed only when showing the
12635 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12636 * iterate over the add-ons just to get the last license data.
12637 */
12638 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12639 } else {
12640 $this->store_last_activated_license_data( $license );
12641 }
12642
12643 if ( $license->is_whitelabeled ) {
12644 // Activated a developer license, data should be hidden.
12645 $is_whitelabeled = true;
12646 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12647 // The account owner activated a regular license key, no need to hide the data.
12648 $is_whitelabeled = false;
12649 }
12650 }
12651
12652 $this->_storage->is_whitelabeled = $is_whitelabeled;
12653
12654 // Reset the whitelabeled status after update.
12655 $this->is_whitelabeled = null;
12656 if ( $this->is_addon() ) {
12657 $parent_fs = $this->get_parent_instance();
12658
12659 if ( is_object( $parent_fs ) ) {
12660 $parent_fs->is_whitelabeled = null;
12661 }
12662 }
12663 }
12664
12665 /**
12666 * @author Leo Fajardo (@leorw)
12667 * @since 2.3.1
12668 *
12669 * @param FS_Plugin_License $license
12670 * @param FS_User $license_user
12671 */
12672 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12673 if ( ! is_object( $license_user ) ) {
12674 $this->_storage->last_license_key = md5( $license->secret_key );
12675 $this->_storage->last_license_user_id = null;
12676 } else {
12677 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12678 $this->_storage->last_license_user_id = $license_user->id;
12679 }
12680 }
12681
12682 /**
12683 * @author Leo Fajardo (@leorw)
12684 * @since 2.3.1
12685 *
12686 * @param bool $ignore_data_debug_mode
12687 *
12688 * @return bool
12689 */
12690 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12691 if ( true !== $this->_storage->is_whitelabeled ) {
12692 return false;
12693 } else if ( $ignore_data_debug_mode ) {
12694 return true;
12695 }
12696
12697 $fs = $this->is_addon() ?
12698 $this->get_parent_instance() :
12699 $this;
12700
12701 return ! $fs->is_data_debug_mode();
12702 }
12703
12704 /**
12705 * @author Leo Fajardo (@leorw)
12706 * @since 2.3.1
12707 *
12708 * @return number
12709 */
12710 function get_last_license_user_id() {
12711 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12712 $this->_storage->last_license_user_id :
12713 null;
12714 }
12715
12716 /**
12717 * @author Leo Fajardo (@leorw)
12718 * @since 2.3.1
12719 *
12720 * @param int $blog_id
12721 * @param bool $ignore_data_debug_mode
12722 *
12723 * @return bool
12724 */
12725 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12726 if ( ! is_null( $blog_id ) ) {
12727 $this->switch_to_blog( $blog_id );
12728 }
12729
12730 if ( ! is_null( $this->is_whitelabeled ) ) {
12731 $is_whitelabeled = $this->is_whitelabeled;
12732 } else {
12733 $is_whitelabeled = false;
12734
12735 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12736
12737 if ( ! $this->has_addons() ) {
12738 $is_whitelabeled = $is_whitelabeled_flag;
12739 } else if ( $is_whitelabeled_flag ) {
12740 $is_whitelabeled = true;
12741 } else {
12742 if ( $this->is_registered() || $this->is_premium() ) {
12743 $addon_ids = $this->get_updated_account_addons();
12744 } else {
12745 $addons = self::get_all_addons();
12746
12747 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12748 $addons[ $this->_plugin->id ] :
12749 array();
12750
12751 $addon_ids = array();
12752 foreach ( $plugin_addons as $addon ) {
12753 $addon_ids[] = $addon->id;
12754 }
12755 }
12756
12757 $installed_addons = $this->get_installed_addons();
12758 foreach ( $installed_addons as $fs_addon ) {
12759 $addon_ids[] = $fs_addon->get_id();
12760 }
12761
12762 if ( ! empty( $addon_ids ) ) {
12763 $addon_ids = array_unique( $addon_ids );
12764
12765 $is_network_level = (
12766 fs_is_network_admin() &&
12767 $this->is_network_active()
12768 );
12769
12770 foreach ( $addon_ids as $addon_id ) {
12771 $addon = $this->get_addon( $addon_id );
12772
12773 if ( ! is_object( $addon ) ) {
12774 continue;
12775 }
12776
12777 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12778 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12779 self::get_addon_instance( $addon_id ) :
12780 null;
12781
12782 $was_addon_network_activated = false;
12783
12784 if ( is_object( $fs_addon ) ) {
12785 $was_addon_network_activated = $fs_addon->is_network_active();
12786 } else if ( $is_network_level ) {
12787 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12788 }
12789
12790 $network_delegated_connection = (
12791 $was_addon_network_activated &&
12792 $addon_storage->get( 'is_delegated_connection', false, true )
12793 );
12794
12795 if (
12796 $is_network_level &&
12797 ( ! $was_addon_network_activated || $network_delegated_connection )
12798 ) {
12799 $sites = self::get_sites();
12800
12801 /**
12802 * If in network admin area and the add-on was not network-activated or network-activated
12803 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12804 */
12805 foreach ( $sites as $site ) {
12806 $site_info = $this->get_site_info( $site );
12807
12808 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12809 $is_whitelabeled = true;
12810 break;
12811 }
12812 }
12813
12814 if ( $is_whitelabeled ) {
12815 break;
12816 }
12817 } else {
12818 /**
12819 * This will be executed when any of the following is met:
12820 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12821 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12822 * 3. Add-on was not network-activated and in site admin area.
12823 */
12824 if ( true === $addon_storage->is_whitelabeled ) {
12825 $is_whitelabeled = true;
12826 break;
12827 }
12828 }
12829 }
12830 }
12831 }
12832
12833 $this->is_whitelabeled = $is_whitelabeled;
12834
12835 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12836 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12837 }
12838
12839 if ( ! is_null( $blog_id ) ) {
12840 $this->restore_current_blog();
12841 }
12842 }
12843
12844 return (
12845 $is_whitelabeled &&
12846 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12847 );
12848 }
12849
12850 /**
12851 * Sync site's subscription.
12852 *
12853 * @author Vova Feldman (@svovaf)
12854 * @since 1.0.9
12855 *
12856 * @param FS_Plugin_License|null $license
12857 *
12858 * @return bool|\FS_Subscription
12859 */
12860 private function _sync_site_subscription( $license ) {
12861 if ( ! is_object( $license ) ) {
12862 $this->delete_unused_subscriptions();
12863
12864 return false;
12865 }
12866
12867 // Load subscription details if not lifetime.
12868 $subscription = $license->is_lifetime() ?
12869 false :
12870 $this->_fetch_site_license_subscription();
12871
12872 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12873 $this->store_subscription( $subscription );
12874 } else {
12875 $this->delete_unused_subscriptions();
12876 }
12877
12878 return $subscription;
12879 }
12880
12881 /**
12882 * @author Vova Feldman (@svovaf)
12883 * @since 1.0.6
12884 *
12885 * @return bool|\FS_Plugin_License
12886 */
12887 function _get_license() {
12888 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12889 return $this->_license;
12890 }
12891
12892 return $this->_get_available_premium_license();
12893 }
12894
12895 /**
12896 * @param number $license_id
12897 *
12898 * @return null|\FS_Subscription
12899 */
12900 function _get_subscription( $license_id ) {
12901 if ( ! isset( $this->_storage->subscriptions ) ||
12902 empty( $this->_storage->subscriptions )
12903 ) {
12904 return null;
12905 }
12906
12907 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12908 if ( $subscription->license_id == $license_id ) {
12909 return $subscription;
12910 }
12911 }
12912
12913 return null;
12914 }
12915
12916 /**
12917 * @author Leo Fajardo (@leorw)
12918 * @since 2.0.0
12919 *
12920 * @param FS_Subscription $subscription
12921 */
12922 function store_subscription( FS_Subscription $subscription ) {
12923 if ( ! isset( $this->_storage->subscriptions ) ) {
12924 $this->_storage->subscriptions = array();
12925 }
12926
12927 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12928 $this->_storage->subscriptions = array( $subscription );
12929
12930 return;
12931 }
12932
12933 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12934
12935 $updated_subscription = false;
12936 foreach ( $subscriptions as $key => $existing_subscription ) {
12937 if ( $existing_subscription->id == $subscription->id ) {
12938 $subscriptions[ $key ] = $subscription;
12939 $updated_subscription = true;
12940 break;
12941 }
12942 }
12943
12944 if ( ! $updated_subscription ) {
12945 $subscriptions[] = $subscription;
12946 }
12947
12948 $this->_storage->subscriptions = $subscriptions;
12949 }
12950
12951 /**
12952 * @author Leo Fajardo (@leorw)
12953 * @since 2.0.0
12954 */
12955 function delete_unused_subscriptions() {
12956 if ( ! isset( $this->_storage->subscriptions ) ||
12957 empty( $this->_storage->subscriptions ) ||
12958 // Clean up only if there are already at least 3 subscriptions.
12959 ( count( $this->_storage->subscriptions ) < 3 )
12960 ) {
12961 return;
12962 }
12963
12964 if ( ! is_multisite() ) {
12965 // If not multisite, there should only be 1 subscription, so just clear the array.
12966 $this->_storage->subscriptions = array();
12967
12968 return;
12969 }
12970
12971 $subscriptions_to_keep_by_license_id_map = array();
12972 $sites = self::get_sites();
12973 foreach ( $sites as $site ) {
12974 $blog_id = self::get_site_blog_id( $site );
12975 $install = $this->get_install_by_blog_id( $blog_id );
12976
12977 if ( ! is_object( $install ) ||
12978 ! FS_Plugin_License::is_valid_id( $install->license_id )
12979 ) {
12980 continue;
12981 }
12982
12983 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12984 }
12985
12986 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12987 $this->_storage->subscriptions = array();
12988
12989 return;
12990 }
12991
12992 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12993 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12994 unset( $this->_storage->subscriptions[ $key ] );
12995 }
12996 }
12997 }
12998
12999 /**
13000 * @author Vova Feldman (@svovaf)
13001 * @since 1.0.2
13002 *
13003 * @param string $plan Plan name
13004 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13005 *
13006 * @return bool
13007 */
13008 function is_plan( $plan, $exact = false ) {
13009 $this->_logger->entrance();
13010
13011 if ( ! $this->is_registered() ) {
13012 return false;
13013 }
13014
13015 $plan = strtolower( $plan );
13016
13017 $current_plan_name = $this->get_plan_name();
13018
13019 if ( $current_plan_name === $plan ) {
13020 // Exact plan.
13021 return true;
13022 } else if ( $exact ) {
13023 // Required exact, but plans are different.
13024 return false;
13025 }
13026
13027 $current_plan_order = - 1;
13028 $required_plan_order = PHP_INT_MAX;
13029 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13030 if ( $plan === $this->_plans[ $i ]->name ) {
13031 $required_plan_order = $i;
13032 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
13033 $current_plan_order = $i;
13034 }
13035 }
13036
13037 return ( $current_plan_order > $required_plan_order );
13038 }
13039
13040 /**
13041 * Check if module has only one plan.
13042 *
13043 * @author Vova Feldman (@svovaf)
13044 * @since 1.2.1.7
13045 *
13046 * @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.
13047 *
13048 * @return bool
13049 */
13050 function is_single_plan( $double_check = false ) {
13051 $this->_logger->entrance();
13052
13053 if ( ! $this->is_registered() ||
13054 ! is_array( $this->_plans ) ||
13055 0 === count( $this->_plans )
13056 ) {
13057 return true;
13058 }
13059
13060 $has_free_plan = $this->has_free_plan();
13061
13062 if ( ! $has_free_plan && $double_check ) {
13063 foreach ( $this->_plans as $plan ) {
13064 if ( $plan->is_free() ) {
13065 $has_free_plan = true;
13066 break;
13067 }
13068 }
13069 }
13070
13071 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
13072 }
13073
13074 /**
13075 * Check if plan based on trial. If not in trial mode, should return false.
13076 *
13077 * @since 1.0.9
13078 *
13079 * @param string $plan Plan name
13080 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13081 *
13082 * @return bool
13083 */
13084 function is_trial_plan( $plan, $exact = false ) {
13085 $this->_logger->entrance();
13086
13087 if ( ! $this->is_registered() ) {
13088 return false;
13089 }
13090
13091 if ( ! $this->is_trial() ) {
13092 return false;
13093 }
13094
13095 $trial_plan = $this->get_trial_plan();
13096
13097 if ( $trial_plan->name === $plan ) {
13098 // Exact plan.
13099 return true;
13100 } else if ( $exact ) {
13101 // Required exact, but plans are different.
13102 return false;
13103 }
13104
13105 $current_plan_order = - 1;
13106 $required_plan_order = - 1;
13107 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13108 if ( $plan === $this->_plans[ $i ]->name ) {
13109 $required_plan_order = $i;
13110 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
13111 $current_plan_order = $i;
13112 }
13113 }
13114
13115 return ( $current_plan_order > $required_plan_order );
13116 }
13117
13118 /**
13119 * Check if plugin has any paid plans.
13120 *
13121 * @author Vova Feldman (@svovaf)
13122 * @since 1.0.7
13123 *
13124 * @return bool
13125 */
13126 function has_paid_plan() {
13127 return $this->_has_paid_plans ||
13128 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
13129 }
13130
13131 /**
13132 * Check if plugin has any plan with a trail.
13133 *
13134 * @author Vova Feldman (@svovaf)
13135 * @since 1.0.9
13136 *
13137 * @return bool
13138 */
13139 function has_trial_plan() {
13140 /**
13141 * @author Vova Feldman(@svovaf)
13142 * @since 1.2.1.5
13143 *
13144 * Allow setting a trial from the SDK without calling the API.
13145 * But, if the user did opt-in, continue using the real data from the API.
13146 */
13147 if ( $this->_trial_days >= 0 ) {
13148 return true;
13149 }
13150
13151 return $this->_storage->get( 'has_trial_plan', false );
13152 }
13153
13154 /**
13155 * Check if plugin has any free plan, or is it premium only.
13156 *
13157 * Note: If no plans configured, assume plugin is free.
13158 *
13159 * @author Vova Feldman (@svovaf)
13160 * @since 1.0.7
13161 *
13162 * @return bool
13163 */
13164 function has_free_plan() {
13165 return ! $this->is_only_premium();
13166 }
13167
13168 /**
13169 * Displays a license activation dialog box when the user clicks on the "Activate License"
13170 * or "Change License" link on the plugins
13171 * page.
13172 *
13173 * @author Leo Fajardo (@leorw)
13174 * @since 1.1.9
13175 */
13176 function _add_license_activation_dialog_box() {
13177 $vars = array(
13178 'id' => $this->_module_id,
13179 );
13180
13181 fs_require_template( 'forms/license-activation.php', $vars );
13182 fs_require_template( 'forms/resend-key.php', $vars );
13183 }
13184
13185 /**
13186 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
13187 *
13188 * @author Leo Fajardo (@leorw)
13189 * @since 2.5.0
13190 */
13191 function _add_email_address_update_dialog_box() {
13192 $vars = array( 'id' => $this->_module_id );
13193
13194 fs_require_template( 'forms/email-address-update.php', $vars );
13195 }
13196
13197 /**
13198 * @author Leo Fajardo (@leorw)
13199 * @since 2.5.0
13200 */
13201 function _add_email_address_update_option() {
13202 if ( ! $this->should_handle_user_change() ) {
13203 return;
13204 }
13205
13206 // Add email address update AJAX handler.
13207 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13208 }
13209
13210 /**
13211 * @author Leo Fajardo (@leorw)
13212 * @since 2.5.0
13213 */
13214 function _email_address_update_ajax_handler() {
13215 $this->check_ajax_referer( 'update_email_address' );
13216
13217 $new_email_address = fs_request_get( 'email_address' );
13218 $transfer_type = fs_request_get( 'transfer_type' );
13219
13220 $result = $this->update_email( $new_email_address );
13221
13222 if ( ! FS_Api::is_api_error( $result ) ) {
13223 self::shoot_ajax_success();
13224 }
13225
13226 $error = '';
13227
13228 if ( FS_Api::is_api_error_object( $result ) ) {
13229 switch ( $result->error->code ) {
13230 case 'user_exist':
13231 case 'account_verification_required':
13232 $error = array(
13233 'code' => 'change_ownership',
13234 'url' => $this->get_account_url( 'change_owner', array(
13235 'state' => 'init',
13236 'candidate_email' => $new_email_address,
13237 'transfer_type' => $transfer_type,
13238 ) ),
13239 );
13240
13241 break;
13242 }
13243 }
13244
13245 if ( empty( $error ) ) {
13246 $error = is_object( $result ) ?
13247 var_export( $result->error, true ) :
13248 $result;
13249 }
13250
13251 self::shoot_ajax_failure( $error );
13252 }
13253
13254 /**
13255 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13256 *
13257 * @author Leo Fajardo (@leorw)
13258 * @since 2.3.2
13259 *
13260 * @return number[]
13261 */
13262 function get_installs_ids_with_foreign_licenses() {
13263 $installs = array();
13264
13265 if (
13266 is_object( $this->_license ) &&
13267 $this->_site->user_id != $this->_license->user_id
13268 ) {
13269 $installs[] = $this->_site->id;
13270 }
13271
13272 /**
13273 * Also try to get foreign licenses for the context product's add-ons.
13274 */
13275 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13276
13277 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13278 if ( $slug == $this->get_slug() ) {
13279 continue;
13280 }
13281
13282 $install = $install_info['install'];
13283 $license = $install_info['license'];
13284
13285 if (
13286 is_object( $license ) &&
13287 $install->user_id != $license->user_id
13288 ) {
13289 $installs[] = $install->id;
13290 }
13291 }
13292
13293 return $installs;
13294 }
13295
13296 /**
13297 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13298 *
13299 * @author Leo Fajardo (@leorw)
13300 * @since 2.3.2
13301 *
13302 * @param number[] $install_ids
13303 */
13304 function _add_user_change_dialog_box( $install_ids ) {
13305 $vars = array(
13306 'id' => $this->_module_id,
13307 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13308 );
13309
13310 fs_require_template( 'forms/user-change.php', $vars );
13311 }
13312
13313 /**
13314 * @author Leo Fajardo (@leorw)
13315 * @since 2.3.1
13316 */
13317 function _add_data_debug_mode_dialog_box() {
13318 $vars = array(
13319 'id' => $this->_module_id,
13320 );
13321
13322 fs_require_template( 'forms/data-debug-mode.php', $vars );
13323 }
13324
13325 /**
13326 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13327 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13328 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13329 * is only activated on a single production site.
13330 *
13331 * @author Leo Fajardo (@leorw)
13332 * @since 2.2.1
13333 *
13334 * @param bool $is_license_deactivation
13335 *
13336 * @return array
13337 */
13338 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13339 if ( fs_is_network_admin() ) {
13340 // Subscription cancellation dialog box is currently not supported for multisite networks.
13341 return array();
13342 }
13343
13344 if ( $this->is_whitelabeled() ) {
13345 return array();
13346 }
13347
13348 $license = $this->_get_license();
13349
13350 /**
13351 * 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.
13352 *
13353 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13354 * @since 2.2.1
13355 */
13356 if ( ! is_object( $license ) ||
13357 $license->is_lifetime() ||
13358 ( ! $license->is_single_site() && $license->activated > 1 )
13359 ) {
13360 return array();
13361 }
13362
13363 /**
13364 * @var FS_Subscription $subscription
13365 */
13366 $subscription = $this->_get_subscription( $license->id );
13367 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13368 return array();
13369 }
13370
13371 return array(
13372 'id' => $this->_module_id,
13373 'license' => $license,
13374 'has_trial' => $this->is_paid_trial(),
13375 'is_license_deactivation' => $is_license_deactivation,
13376 );
13377 }
13378
13379 /**
13380 * @author Leo Fajardo (@leorw)
13381 * @since 2.0.2
13382 */
13383 function _add_premium_version_upgrade_selection_dialog_box() {
13384 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13385 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13386 return;
13387 }
13388
13389 $vars = array(
13390 'id' => $this->_module_id,
13391 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13392 $modules_update->response[ $this->_plugin_basename ]->new_version :
13393 $modules_update->response[ $this->_plugin_basename ]['new_version']
13394 );
13395
13396 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13397 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13398 }
13399
13400 /**
13401 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13402 * page.
13403 *
13404 * @author Leo Fajardo (@leorw)
13405 * @since 1.2.1.5
13406 */
13407 function _add_optout_dialog() {
13408 if ( $this->is_theme() ) {
13409 $vars = null;
13410 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13411 }
13412
13413 $vars = array( 'id' => $this->_module_id );
13414 fs_require_template( 'forms/optout.php', $vars );
13415 }
13416
13417 /**
13418 * Prepare page to include all required UI and logic for the license activation dialog.
13419 *
13420 * @author Vova Feldman (@svovaf)
13421 * @since 1.2.0
13422 */
13423 function _add_license_activation() {
13424 if ( $this->is_migration() ) {
13425 return;
13426 }
13427
13428 if ( ! $this->is_user_admin() ) {
13429 // Only admins can activate a license.
13430 return;
13431 }
13432
13433 if ( ! $this->has_paid_plan() ) {
13434 // Module doesn't have any paid plans.
13435 return;
13436 }
13437
13438 if (
13439 $this->has_premium_version() &&
13440 ! $this->is_premium() &&
13441 /**
13442 * Also handle the case when an upgrade was made using the free version.
13443 *
13444 * @author Leo Fajardo (@leorw)
13445 * @since 2.3.2
13446 */
13447 ! is_object( $this->_get_license() )
13448 ) {
13449 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13450 return;
13451 }
13452
13453 // Add license activation link and AJAX request handler.
13454 if ( self::is_plugins_page() ) {
13455 $is_network_admin = fs_is_network_admin();
13456
13457 if (
13458 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13459 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13460 ) {
13461 if (
13462 $this->is_premium() ||
13463 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13464 ) {
13465 /**
13466 * @since 1.2.0 Add license action link only on plugins page.
13467 */
13468 $this->_add_license_action_link();
13469 }
13470 }
13471 }
13472
13473 // Add license activation AJAX callback.
13474 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13475
13476 // Add resend license AJAX callback.
13477 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13478 }
13479
13480 /**
13481 * Prepares page to include all required UI and logic for the "Change User" dialog.
13482 *
13483 * @author Leo Fajardo (@leorw)
13484 * @since 2.3.2
13485 */
13486 function _add_user_change_option() {
13487 if ( ! $this->should_handle_user_change() ) {
13488 return;
13489 }
13490
13491 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13492
13493 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13494 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13495 return;
13496 }
13497
13498 // Add user change AJAX handler.
13499 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13500 }
13501
13502 /**
13503 * @author Leo Fajardo (@leorw)
13504 * @since 2.3.2
13505 */
13506 function should_handle_user_change() {
13507 if ( ! $this->is_user_admin() ) {
13508 // Only admins can change user.
13509 return false;
13510 }
13511
13512 if ( $this->is_addon() ) {
13513 return false;
13514 }
13515
13516 if ( ! $this->is_registered() ) {
13517 return false;
13518 }
13519
13520 if (
13521 $this->is_network_active() &&
13522 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13523 ) {
13524 // Handle only on site-level "Account" section for now.
13525 return false;
13526 }
13527
13528 return true;
13529 }
13530
13531 /**
13532 * @author Leo Fajardo (@leorw)
13533 * @since 2.0.2
13534 */
13535 function _add_premium_version_upgrade_selection() {
13536 if ( ! $this->is_user_admin() ) {
13537 return;
13538 }
13539
13540 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13541 // This is relevant only to the free versions and premium versions without an active license.
13542 return;
13543 }
13544
13545 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13546 $this->_add_premium_version_upgrade_selection_action();
13547 }
13548 }
13549
13550 /**
13551 * @author Edgar Melkonyan
13552 * @since 2.4.1
13553 *
13554 * @throws Freemius_Exception
13555 */
13556 function _toggle_whitelabel_mode_ajax_handler() {
13557 $this->_logger->entrance();
13558
13559 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13560
13561 if ( ! $this->is_user_admin() ) {
13562 // Only for admins.
13563 self::shoot_ajax_failure();
13564 }
13565
13566 $license = $this->get_api_user_scope()->call(
13567 "/licenses/{$this->_site->license_id}.json",
13568 'put',
13569 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13570 );
13571
13572 if ( ! $this->is_api_result_entity( $license ) ) {
13573 self::shoot_ajax_failure(
13574 FS_Api::is_api_error_object( $license ) ?
13575 $license->error->message :
13576 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13577 );
13578 }
13579
13580 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13581 $this->_store_licenses();
13582
13583 $this->_sync_license();
13584
13585 if ( ! $license->is_whitelabeled ) {
13586 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13587 } else {
13588 $this->_admin_notices->add_sticky(
13589 sprintf(
13590 $this->get_text_inline(
13591 '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.',
13592 'license_whitelabeled'
13593 ),
13594 "<strong>{$this->get_plugin_title()}</strong>",
13595 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13596 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13597 ),
13598 'license_whitelabeled'
13599 );
13600 }
13601
13602 self::shoot_ajax_response( array( 'success' => true ) );
13603 }
13604
13605 /**
13606 * @author Leo Fajardo (@leorw)
13607 * @since 2.3.0
13608 */
13609 function _add_beta_mode_update_handler() {
13610 if ( ! $this->is_user_admin() ) {
13611 return;
13612 }
13613
13614 if ( ! $this->is_premium() ) {
13615 return;
13616 }
13617
13618 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13619 }
13620
13621 /**
13622 * @author Leo Fajardo (@leorw)
13623 * @since 2.3.0
13624 */
13625 function _set_beta_mode_ajax_handler() {
13626 $this->_logger->entrance();
13627
13628 $this->check_ajax_referer( 'set_beta_mode' );
13629
13630 if ( ! $this->is_user_admin() ) {
13631 // Only for admins.
13632 self::shoot_ajax_failure();
13633 }
13634
13635 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13636
13637 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13638 self::shoot_ajax_failure();
13639 }
13640
13641 $site = $this->api_site_call(
13642 '',
13643 'put',
13644 array(
13645 'is_beta' => ( 'true' == $is_beta ),
13646 'fields' => 'is_beta'
13647 )
13648 );
13649
13650 if ( ! $this->is_api_result_entity( $site ) ) {
13651 self::shoot_ajax_failure(
13652 FS_Api::is_api_error_object( $site ) ?
13653 $site->error->message :
13654 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13655 );
13656 }
13657
13658 $this->_site->is_beta = $site->is_beta;
13659 $this->_store_site();
13660
13661 self::shoot_ajax_response( array( 'success' => true ) );
13662 }
13663
13664 /**
13665 * License activation WP AJAX handler.
13666 *
13667 * @author Leo Fajardo (@leorw)
13668 * @since 1.1.9
13669 *
13670 * @uses Freemius::activate_license()
13671 */
13672 function _activate_license_ajax_action() {
13673 $this->_logger->entrance();
13674
13675 $this->check_ajax_referer( 'activate_license' );
13676
13677 $license_key = trim( fs_request_get( 'license_key' ) );
13678
13679 if ( empty( $license_key ) ) {
13680 exit;
13681 }
13682
13683 $sites = fs_is_network_admin() ?
13684 fs_request_get( 'sites', array(), 'post' ) :
13685 array();
13686
13687 $result = $this->activate_license(
13688 $license_key,
13689 $sites,
13690 fs_request_get_bool( 'is_marketing_allowed', null ),
13691 fs_request_get( 'blog_id', null ),
13692 fs_request_get( 'module_id', null, 'post' ),
13693 fs_request_get( 'user_id', null ),
13694 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13695 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13696 );
13697
13698 if (
13699 $result['success'] &&
13700 $this->is_bundle_license_auto_activation_enabled()
13701 ) {
13702 $license = new FS_Plugin_License();
13703 $license->secret_key = $license_key;
13704
13705 $this->maybe_activate_bundle_license( $license, $sites );
13706 }
13707
13708 echo json_encode( $result );
13709
13710 exit;
13711 }
13712
13713 /**
13714 * User change WP AJAX handler.
13715 *
13716 * @author Leo Fajardo (@leorw)
13717 * @since 2.3.2
13718 */
13719 function _user_change_ajax_action() {
13720 $this->_logger->entrance();
13721
13722 $this->check_ajax_referer( 'change_user' );
13723
13724 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13725 $new_user_id = fs_request_get( 'user_id' );
13726
13727 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13728 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13729 }
13730
13731 $params = array();
13732
13733 if ( ! empty( $new_email_address ) ) {
13734 $params['user_email'] = $new_email_address;
13735 } else {
13736 $params['user_id'] = $new_user_id;
13737 }
13738
13739 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13740 $install_ids = array();
13741
13742 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13743 $install_ids[ $slug ] = $install_info['install']->id;
13744 }
13745
13746 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13747
13748 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13749
13750 if ( FS_Api::is_api_error( $install ) ) {
13751 $error = '';
13752
13753 if ( is_object( $install ) ) {
13754 switch ( $install->error->code ) {
13755 case 'user_exist':
13756 $error = (
13757 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13758 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13759 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>' ) .
13760 sprintf(
13761 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13762 $this->get_account_url( 'change_owner', array(
13763 'state' => 'init',
13764 'candidate_email' => $new_email_address
13765 ) ),
13766 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13767 )
13768 );
13769 break;
13770 }
13771 }
13772
13773 if ( empty( $error ) ) {
13774 $error = FS_Api::is_api_error_object( $install ) ?
13775 $install->error->message :
13776 var_export( $install->error, true );
13777 }
13778
13779 self::shoot_ajax_failure( $error );
13780 } else {
13781 if (
13782 // If successful ownership change.
13783 $this->get_user()->id != $install->user_id ||
13784 ! empty( $new_email_address )
13785 ) {
13786 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13787 }
13788 }
13789
13790 self::shoot_ajax_success();
13791 }
13792
13793 /**
13794 * @author Leo Fajardo (@leorw)
13795 * @since 2.3.2.14
13796 */
13797 function starting_migration() {
13798 if ( ! empty( $this->_storage->license_migration ) ) {
13799 // Do not overwrite the data if already set.
13800 return;
13801 }
13802
13803 $this->_storage->license_migration = array(
13804 'is_migrating' => true,
13805 'start_timestamp' => time()
13806 );
13807 }
13808
13809 /**
13810 * @author Leo Fajardo (@leorw)
13811 * @since 2.3.2.14
13812 */
13813 function is_migration() {
13814 if ( $this->is_addon() ) {
13815 return $this->get_parent_instance()->is_migration();
13816 }
13817
13818 if ( empty( $this->_storage->license_migration ) ) {
13819 return false;
13820 }
13821
13822 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13823 return false;
13824 }
13825
13826 return (
13827 // Return `true` if the migration is within 5 minutes from the starting time.
13828 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13829 );
13830 }
13831
13832 /**
13833 *
13834 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13835 *
13836 * @author Vova Feldman (@svovaf)
13837 * @since 2.3.0
13838 *
13839 * @param string $license_key
13840 * @param null|bool $is_marketing_allowed
13841 * @param null|number $plugin_id
13842 * @param array $sites
13843 * @param int $blog_id
13844 *
13845 * @return array {
13846 * @var bool $success
13847 * @var string $error
13848 * @var string $next_page
13849 * }
13850 *
13851 * @uses Freemius::activate_license()
13852 */
13853 function activate_migrated_license(
13854 $license_key,
13855 $is_marketing_allowed = null,
13856 $plugin_id = null,
13857 $sites = array(),
13858 $blog_id = null
13859 ) {
13860 $this->_logger->entrance();
13861
13862 $result = $this->activate_license(
13863 $license_key,
13864 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13865 $this->get_sites_for_network_level_optin() :
13866 $sites,
13867 $is_marketing_allowed,
13868 $blog_id,
13869 $plugin_id
13870 );
13871
13872 // No need to show the sticky after license activation notice after migrating a license.
13873 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13874
13875 return $result;
13876 }
13877
13878 /**
13879 * @author Leo Fajardo (@leorw)
13880 * @since 2.3.1
13881 *
13882 * @return string
13883 */
13884 function get_pricing_js_path() {
13885 if ( ! isset( $this->_pricing_js_path ) ) {
13886 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
13887
13888 if ( empty( $pricing_js_path ) ) {
13889 global $fs_active_plugins;
13890
13891 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13892 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13893 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13894
13895 $pricing_js_path = $plugin_or_theme_root_dir
13896 . '/'
13897 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13898 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13899 . '/includes/freemius-pricing/freemius-pricing.js';
13900
13901 break;
13902 }
13903 }
13904 }
13905
13906 $this->_pricing_js_path = $pricing_js_path;
13907 }
13908
13909 return $this->_pricing_js_path;
13910 }
13911
13912 /**
13913 * @author Leo Fajardo (@leorw)
13914 * @since 2.3.1
13915 *
13916 * @return bool
13917 */
13918 function should_use_external_pricing() {
13919 if ( is_null( $this->_use_external_pricing ) ) {
13920 $pricing_js_path = $this->get_pricing_js_path();
13921
13922 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
13923 }
13924
13925 return $this->_use_external_pricing;
13926 }
13927
13928 /**
13929 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13930 *
13931 * @author Vova Feldman (@svovaf)
13932 * @since 2.2.4
13933 * @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).
13934 * @param string $license_key
13935 * @param array $sites
13936 * @param null|bool $is_marketing_allowed
13937 * @param null|int $blog_id
13938 * @param null|number $plugin_id
13939 * @param null|number $license_owner_id
13940 * @param bool|null $is_extensions_tracking_allowed
13941 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13942 *
13943 *
13944 * @return array {
13945 * @var bool $success
13946 * @var string $error
13947 * @var string $next_page
13948 * }
13949 */
13950 private function activate_license(
13951 $license_key,
13952 $sites = array(),
13953 $is_marketing_allowed = null,
13954 $blog_id = null,
13955 $plugin_id = null,
13956 $license_owner_id = null,
13957 $is_extensions_tracking_allowed = null,
13958 $is_diagnostic_tracking_allowed = null
13959 ) {
13960 $this->_logger->entrance();
13961
13962 $license_key = trim( $license_key );
13963
13964 $is_network_activation_or_migration = (
13965 fs_is_network_admin() ||
13966 ( ! empty( $sites ) && $this->is_migration() )
13967 );
13968
13969 if ( ! $is_network_activation_or_migration ) {
13970 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13971 $sites = array();
13972 }
13973
13974 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13975 $this :
13976 $this->get_addon_instance( $plugin_id );
13977
13978 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
13979 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
13980 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
13981 ) );
13982
13983 $error = false;
13984 $next_page = false;
13985
13986 $has_valid_blog_id = is_numeric( $blog_id );
13987
13988 $user = null;
13989
13990 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13991 /**
13992 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13993 *
13994 * @author Vova Feldman (@svovaf)
13995 */
13996 $user = $fs->get_parent_instance()->get_current_or_network_user();
13997 } else if ( $fs->is_registered() ) {
13998 $user = $fs->get_current_or_network_user();
13999 }
14000
14001 if ( $has_valid_blog_id ) {
14002 /**
14003 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
14004 *
14005 * @author Leo Fajardo (@leorw)
14006 */
14007 $fs->switch_to_blog( $blog_id );
14008 }
14009
14010 if ( is_object( $user ) ) {
14011 $result = true;
14012
14013 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
14014 // If no specific blog ID was provided, activate the license for all sites in the network.
14015 $blog_2_install_map = array();
14016 $site_ids = array();
14017
14018 foreach ( $sites as $site ) {
14019 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
14020 continue;
14021 }
14022
14023 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
14024
14025 if ( is_object( $install ) ) {
14026 $blog_2_install_map[ $site['blog_id'] ] = $install;
14027 } else {
14028 $site_ids[] = $site['blog_id'];
14029 }
14030 }
14031
14032 if ( ! empty( $blog_2_install_map ) ) {
14033 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
14034 }
14035
14036 if ( true === $result && ! empty( $site_ids ) ) {
14037 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
14038 }
14039 } else {
14040 if ( $fs->is_registered() ) {
14041 $params = array(
14042 'license_key' => $fs->apply_filters( 'license_key', $license_key )
14043 );
14044
14045 $install_ids = array();
14046
14047 $change_owner = FS_User::is_valid_id( $license_owner_id );
14048
14049 if ( $change_owner ) {
14050 $params['user_id'] = $license_owner_id;
14051
14052 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
14053
14054 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
14055 $install_ids[ $slug ] = $install_info['install']->id;
14056 }
14057
14058 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
14059 }
14060
14061 $api = $fs->get_api_site_scope();
14062
14063 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
14064
14065 if ( ! FS_Api::is_api_error( $result ) ) {
14066 $install = $result;
14067
14068 $fs->reconnect_locally( $has_valid_blog_id );
14069
14070 if (
14071 $change_owner &&
14072 // If successful ownership change.
14073 $fs->get_user()->id != $install->user_id
14074 ) {
14075 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
14076 }
14077 }
14078 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
14079 $result = $fs->activate_license_on_site( $user, $license_key );
14080 }
14081 }
14082
14083 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
14084 if ( FS_Api::is_blocked( $result ) ) {
14085 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
14086 }
14087
14088 $error = FS_Api::is_api_error_object( $result ) ?
14089 $result->error->message :
14090 var_export( $result, true );
14091 } else {
14092 $fs->network_upgrade_mode_completed();
14093
14094 $fs->_user = $user;
14095
14096 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
14097 $fs->_site = $fs->get_network_install();
14098 }
14099
14100 $fs->_sync_license( true, $has_valid_blog_id );
14101
14102 $this->maybe_sync_install_user();
14103
14104 $next_page = $fs->is_addon() ?
14105 $fs->get_parent_instance()->get_account_url() :
14106 $fs->get_after_activation_url( 'after_connect_url' );
14107 }
14108 } else {
14109 $next_page = $fs->opt_in(
14110 false,
14111 false,
14112 false,
14113 $license_key,
14114 false,
14115 false,
14116 false,
14117 $is_marketing_allowed,
14118 $sites
14119 );
14120
14121 if ( isset( $next_page->error ) ) {
14122 $error = $next_page->error;
14123 } else {
14124 if ( $is_network_activation_or_migration ) {
14125 /**
14126 * Get the list of sites that were just opted-in (and license activated).
14127 * This is an optimization for the next part below saving some DB queries.
14128 */
14129 $connected_sites = array();
14130 foreach ( $sites as $site ) {
14131 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
14132 $connected_sites[ $site['blog_id'] ] = true;
14133 }
14134 }
14135
14136 $all_sites = self::get_sites();
14137 $pending_blog_ids = array();
14138
14139 /**
14140 * 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.
14141 *
14142 * @author Vova Feldman (@svovaf)
14143 */
14144 foreach ( $all_sites as $site ) {
14145 $blog_id = self::get_site_blog_id( $site );
14146
14147 if ( isset( $connected_sites[ $blog_id ] ) ) {
14148 // Site was just connected.
14149 continue;
14150 }
14151
14152 if ( $fs->is_installed_on_site( $blog_id ) ) {
14153 // Site was already connected before.
14154 continue;
14155 }
14156
14157 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
14158 // Site's connection was delegated.
14159 continue;
14160 }
14161
14162 if ( $fs->is_anonymous_site( $blog_id ) ) {
14163 // Site connection was already skipped.
14164 continue;
14165 }
14166
14167 $pending_blog_ids[] = $blog_id;
14168 }
14169
14170 if ( ! empty( $pending_blog_ids ) ) {
14171 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14172 $fs->skip_connection( $pending_blog_ids );
14173 } else {
14174 $fs->delegate_connection( $pending_blog_ids );
14175 }
14176 }
14177 }
14178 }
14179 }
14180
14181 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14182 $fs->_storage->require_license_activation = false;
14183 }
14184
14185 $result = array(
14186 'success' => ( false === $error )
14187 );
14188
14189 if ( false !== $error ) {
14190 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14191 } else {
14192 if ( $fs->is_addon() || $fs->has_addons() ) {
14193 /**
14194 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14195 * an updated valid user licenses collection will be fetched from the server which is used to also
14196 * update the account add-ons (add-ons the user has licenses for).
14197 *
14198 * @author Leo Fajardo (@leorw)
14199 * @since 2.2.4
14200 */
14201 $fs->purge_valid_user_licenses_cache();
14202 }
14203
14204 $result['next_page'] = $next_page;
14205 }
14206
14207 return $result;
14208 }
14209
14210 /**
14211 * @author Leo Fajardo (@leorw)
14212 * @since 2.3.2
14213 *
14214 * @return array {
14215 * @key string Product slug.
14216 * @value array {
14217 * @property FS_Site $site
14218 * @property FS_Plugin_License $license
14219 * }
14220 * }
14221 */
14222 private function get_parent_and_addons_installs_info() {
14223 $fs = $this->is_addon() ?
14224 $this->get_parent_instance() :
14225 $this;
14226
14227 $installed_addons_ids = array();
14228
14229 $installed_addons_instances = $fs->get_installed_addons();
14230 foreach ( $installed_addons_instances as $instance ) {
14231 $installed_addons_ids[] = $instance->get_id();
14232 }
14233
14234 $addons_ids = array_unique( array_merge(
14235 $installed_addons_ids,
14236 $fs->get_updated_account_addons()
14237 ) );
14238
14239 // Add parent product info.
14240 $installs_info_by_slug_map = array(
14241 $fs->get_slug() => array(
14242 'install' => $fs->get_site(),
14243 'license' => $fs->_get_license()
14244 )
14245 );
14246
14247 foreach ( $addons_ids as $addon_id ) {
14248 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14249
14250 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14251
14252 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14253 // Add-on is not associated with an install entity.
14254 continue;
14255 }
14256
14257 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14258 'install' => $addon_info['site'],
14259 'license' => isset( $addon_info['license'] ) ?
14260 $addon_info['license'] :
14261 null
14262 );
14263 }
14264
14265 return $installs_info_by_slug_map;
14266 }
14267
14268 /**
14269 * @author Leo Fajardo (@leorw)
14270 * @since 1.2.3.1
14271 */
14272 function _network_activate_ajax_action() {
14273 $this->_logger->entrance();
14274
14275 $this->check_ajax_referer( 'network_activate' );
14276
14277 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14278 $fs = ( $plugin_id == $this->_module_id ) ?
14279 $this :
14280 $this->get_addon_instance( $plugin_id );
14281
14282 $error = false;
14283
14284 $sites = fs_request_get( 'sites', array(), 'post' );
14285 if ( is_array( $sites ) && ! empty( $sites ) ) {
14286 $sites_by_action = array(
14287 'allow' => array(),
14288 'delegate' => array(),
14289 'skip' => array()
14290 );
14291
14292 foreach ( $sites as $site ) {
14293 $sites_by_action[ $site['action'] ][] = $site;
14294 }
14295
14296 $total_sites = count( $sites );
14297 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14298
14299 $next_page = '';
14300
14301 $has_any_install = fs_request_get_bool( 'has_any_install' );
14302
14303 if ( $total_sites === $total_sites_to_delegate &&
14304 ! $this->is_network_upgrade_mode() &&
14305 ! $has_any_install
14306 ) {
14307 $this->delegate_connection();
14308 } else {
14309 if ( ! empty( $sites_by_action['delegate'] ) ) {
14310 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14311 }
14312
14313 if ( ! empty( $sites_by_action['skip'] ) ) {
14314 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14315 }
14316
14317 if ( empty( $sites_by_action['allow'] ) ) {
14318 if ( $has_any_install ) {
14319 $first_install = $fs->find_first_install();
14320
14321 if ( ! is_null( $first_install ) ) {
14322 $fs->_site = $first_install['install'];
14323 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14324
14325 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14326 $fs->_storage->network_user_id = $fs->_user->id;
14327 }
14328 }
14329 } else {
14330 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14331 $next_page = $fs->opt_in(
14332 false,
14333 false,
14334 false,
14335 false,
14336 false,
14337 false,
14338 false,
14339 fs_request_get_bool( 'is_marketing_allowed', null ),
14340 $sites_by_action['allow']
14341 );
14342 } else {
14343 $next_page = $fs->install_with_user(
14344 $this->get_network_user(),
14345 false,
14346 false,
14347 false,
14348 true,
14349 $sites_by_action['allow']
14350 );
14351 }
14352
14353 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14354 $error = $next_page->error;
14355 }
14356 }
14357 }
14358
14359 if ( empty( $next_page ) ) {
14360 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14361 }
14362 } else {
14363 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14364 }
14365
14366 $result = array(
14367 'success' => ( false === $error )
14368 );
14369
14370 if ( false !== $error ) {
14371 $result['error'] = $error;
14372 } else {
14373 $result['next_page'] = $next_page;
14374 }
14375
14376 echo json_encode( $result );
14377
14378 exit;
14379 }
14380
14381 /**
14382 * Billing update AJAX callback.
14383 *
14384 * @author Vova Feldman (@svovaf)
14385 * @since 1.2.1.5
14386 */
14387 function _update_billing_ajax_action() {
14388 $this->_logger->entrance();
14389
14390 $this->check_ajax_referer( 'update_billing' );
14391
14392 if ( ! $this->is_user_admin() ) {
14393 // Only for admins.
14394 self::shoot_ajax_failure();
14395 }
14396
14397 $billing = fs_request_get( 'billing' );
14398
14399 $api = $this->get_api_user_scope();
14400 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14401 'plugin_id' => $this->get_parent_id(),
14402 ) ) );
14403
14404 if ( ! $this->is_api_result_entity( $result ) ) {
14405 self::shoot_ajax_failure();
14406 }
14407
14408 // Purge cached billing.
14409 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14410
14411 self::shoot_ajax_success();
14412 }
14413
14414 /**
14415 * Trial start for anonymous users (AJAX callback).
14416 *
14417 * @author Vova Feldman (@svovaf)
14418 * @since 1.2.1.5
14419 */
14420 function _start_trial_ajax_action() {
14421 $this->_logger->entrance();
14422
14423 $this->check_ajax_referer( 'start_trial' );
14424
14425 if ( ! $this->is_user_admin() ) {
14426 // Only for admins.
14427 self::shoot_ajax_failure();
14428 }
14429
14430 $trial_data = fs_request_get( 'trial' );
14431
14432 $next_page = $this->opt_in(
14433 false,
14434 false,
14435 false,
14436 false,
14437 false,
14438 $trial_data['plan_id']
14439 );
14440
14441 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14442 self::shoot_ajax_failure(
14443 isset( $next_page->error ) ?
14444 $next_page->error->message :
14445 var_export( $next_page, true )
14446 );
14447 }
14448
14449 $this->shoot_ajax_success( array(
14450 'next_page' => $next_page,
14451 ) );
14452 }
14453
14454 /**
14455 * @author Leo Fajardo (@leorw)
14456 * @since 1.2.0
14457 */
14458 function _resend_license_key_ajax_action() {
14459 $this->_logger->entrance();
14460
14461 $this->check_ajax_referer( 'resend_license_key' );
14462
14463 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14464
14465 if ( empty( $email_address ) ) {
14466 exit;
14467 }
14468
14469 $error = false;
14470
14471 $api = $this->get_api_plugin_scope();
14472 $result = $api->call( '/licenses/resend.json', 'post',
14473 array(
14474 'email' => $email_address,
14475 'url' => home_url(),
14476 )
14477 );
14478
14479 if ( is_object( $result ) && isset( $result->error ) ) {
14480 $error = $result->error;
14481
14482 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14483 $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' );
14484 } else if ( 'no_license' === $error->code ) {
14485 $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' );
14486 } else {
14487 $error = $error->message;
14488 }
14489 }
14490
14491 $licenses = array(
14492 'success' => ( false === $error )
14493 );
14494
14495 if ( false !== $error ) {
14496 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14497 }
14498
14499 echo json_encode( $licenses );
14500
14501 exit;
14502 }
14503
14504 /**
14505 * @author Vova Feldman (@svovaf)
14506 * @since 1.2.1.8
14507 *
14508 * @var string
14509 */
14510 private static $_pagenow;
14511
14512 /**
14513 * Get current page or the referer if executing a WP AJAX request.
14514 *
14515 * @author Vova Feldman (@svovaf)
14516 * @since 1.2.1.8
14517 *
14518 * @return string
14519 */
14520 static function get_current_page() {
14521 if ( ! isset( self::$_pagenow ) ) {
14522 global $pagenow;
14523 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14524 /**
14525 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14526 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14527 *
14528 * @author Leo Fajardo (@leorw)
14529 * @since 2.2.3
14530 */
14531 if ( is_network_admin() ) {
14532 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14533 } else if ( is_user_admin() ) {
14534 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14535 } else {
14536 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14537 }
14538
14539 $pagenow = $self_matches[1];
14540 $pagenow = trim( $pagenow, '/' );
14541 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14542 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14543 $pagenow = 'index.php';
14544 } else {
14545 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14546 $pagenow = strtolower( $self_matches[1] );
14547 if ( '.php' !== substr($pagenow, -4, 4) )
14548 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14549 }
14550 }
14551
14552 self::$_pagenow = $pagenow;
14553
14554 if ( self::is_ajax() &&
14555 'admin-ajax.php' === $pagenow
14556 ) {
14557 $referer = fs_get_raw_referer();
14558
14559 if ( is_string( $referer ) ) {
14560 $parts = explode( '?', $referer );
14561
14562 self::$_pagenow = basename( $parts[0] );
14563 }
14564 }
14565 }
14566
14567 return self::$_pagenow;
14568 }
14569
14570 /**
14571 * Helper method to check if user in the plugins page.
14572 *
14573 * @author Vova Feldman (@svovaf)
14574 * @since 1.2.1.5
14575 *
14576 * @return bool
14577 */
14578 static function is_plugins_page() {
14579 return ( 'plugins.php' === self::get_current_page() );
14580 }
14581
14582 /**
14583 * @author Leo Fajardo (@leorw)
14584 * @since 2.2.3
14585 *
14586 * @return bool
14587 */
14588 static function is_plugin_install_page() {
14589 return ( 'plugin-install.php' === self::get_current_page() );
14590 }
14591
14592 /**
14593 * @author Leo Fajardo (@leorw)
14594 * @since 2.0.2
14595 *
14596 * @return bool
14597 */
14598 static function is_updates_page() {
14599 return ( 'update-core.php' === self::get_current_page() );
14600 }
14601
14602 /**
14603 * Helper method to check if user in the themes page.
14604 *
14605 * @author Vova Feldman (@svovaf)
14606 * @since 1.2.2.6
14607 *
14608 * @return bool
14609 */
14610 static function is_themes_page() {
14611 return ( 'themes.php' === self::get_current_page() );
14612 }
14613
14614 #----------------------------------------------------------------------------------
14615 #region Affiliation
14616 #----------------------------------------------------------------------------------
14617
14618 /**
14619 * @author Leo Fajardo (@leorw)
14620 * @since 1.2.3
14621 *
14622 * @return bool
14623 */
14624 function has_affiliate_program() {
14625 if ( ! is_object( $this->_plugin ) ) {
14626 return false;
14627 }
14628
14629 return $this->_plugin->has_affiliate_program();
14630 }
14631
14632 /**
14633 * Get Plugin ID under which we will track affiliate application.
14634 *
14635 * This could either be the Bundle ID or the main plugin ID.
14636 *
14637 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14638 */
14639 private function get_plugin_id_for_affiliate_terms() {
14640 return $this->has_bundle_context() ?
14641 $this->get_bundle_id() :
14642 $this->_plugin->id;
14643 }
14644
14645 /**
14646 * @author Leo Fajardo (@leorw)
14647 * @since 1.2.4
14648 */
14649 private function fetch_affiliate_terms() {
14650 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14651 /**
14652 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14653 */
14654 $plugins_api = $this->has_bundle_context() ?
14655 $this->get_api_bundle_scope() :
14656 $this->get_api_plugin_scope();
14657
14658 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14659
14660 /**
14661 * 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.
14662 */
14663 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14664 return;
14665 }
14666
14667 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14668 }
14669 }
14670
14671 /**
14672 * @author Leo Fajardo (@leorw)
14673 * @since 1.2.4
14674 */
14675 private function fetch_affiliate_and_custom_terms() {
14676 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14677 $application_data = $this->_storage->affiliate_application_data;
14678 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14679
14680 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14681
14682 $users_api = $this->get_api_user_scope();
14683 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14684 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14685 if ( ! empty( $result->affiliates ) ) {
14686 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14687
14688 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14689 $application_data['status'] = $affiliate->status;
14690 $this->_storage->affiliate_application_data = $application_data;
14691 }
14692
14693 if ( $affiliate->is_using_custom_terms ) {
14694 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14695 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14696 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14697 }
14698 }
14699
14700 $this->affiliate = $affiliate;
14701 }
14702 }
14703 }
14704 }
14705
14706 /**
14707 * @author Leo Fajardo (@leorw)
14708 * @since 1.2.3
14709 */
14710 private function fetch_affiliate_and_terms() {
14711 $this->_logger->entrance();
14712
14713 $this->fetch_affiliate_terms();
14714 $this->fetch_affiliate_and_custom_terms();
14715 }
14716
14717 /**
14718 * @author Leo Fajardo (@leorw)
14719 * @since 1.2.3
14720 *
14721 * @return FS_Affiliate
14722 */
14723 function get_affiliate() {
14724 return $this->affiliate;
14725 }
14726
14727
14728 /**
14729 * @author Leo Fajardo (@leorw)
14730 * @since 1.2.3
14731 *
14732 * @return FS_AffiliateTerms
14733 */
14734 function get_affiliate_terms() {
14735 return is_object( $this->custom_affiliate_terms ) ?
14736 $this->custom_affiliate_terms :
14737 $this->plugin_affiliate_terms;
14738 }
14739
14740 /**
14741 * @author Leo Fajardo (@leorw)
14742 * @since 1.2.3
14743 */
14744 function _submit_affiliate_application() {
14745 $this->_logger->entrance();
14746
14747 $this->check_ajax_referer( 'submit_affiliate_application' );
14748
14749 if ( ! $this->is_user_admin() ) {
14750 // Only for admins.
14751 self::shoot_ajax_failure();
14752 }
14753
14754 $affiliate = fs_request_get( 'affiliate' );
14755
14756 if ( empty( $affiliate['promotion_methods'] ) ) {
14757 unset( $affiliate['promotion_methods'] );
14758 }
14759
14760 if ( ! empty( $affiliate['additional_domains'] ) ) {
14761 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14762 }
14763
14764 if ( ! $this->is_registered() ) {
14765 // Opt in but don't track usage.
14766 $next_page = $this->opt_in(
14767 false,
14768 false,
14769 false,
14770 false,
14771 false,
14772 false,
14773 true
14774 );
14775
14776 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14777 self::shoot_ajax_failure(
14778 isset( $next_page->error ) ?
14779 $next_page->error->message :
14780 var_export( $next_page, true )
14781 );
14782 } else if ( $this->is_pending_activation() ) {
14783 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' ) );
14784 }
14785 }
14786
14787 $this->fetch_affiliate_terms();
14788
14789 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14790
14791 $api = $this->get_api_user_scope();
14792 $result = $api->call(
14793 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14794 'post',
14795 $affiliate
14796 );
14797
14798 if ( $this->is_api_error( $result ) ) {
14799 self::shoot_ajax_failure(
14800 isset( $result->error ) ?
14801 $result->error->message :
14802 var_export( $result, true )
14803 );
14804 } else {
14805 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14806 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14807 }
14808
14809 $affiliate_application_data = array(
14810 'status' => 'pending',
14811 'stats_description' => $affiliate['stats_description'],
14812 'promotion_method_description' => $affiliate['promotion_method_description'],
14813 );
14814
14815 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14816 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14817 }
14818
14819 if ( ! empty( $affiliate['domain'] ) ) {
14820 $affiliate_application_data['domain'] = $affiliate['domain'];
14821 }
14822
14823 if ( ! empty( $affiliate['additional_domains'] ) ) {
14824 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14825 }
14826
14827 $this->_storage->affiliate_application_data = $affiliate_application_data;
14828 }
14829
14830 // Purge cached affiliate.
14831 $api->purge_cache( 'affiliate.json' );
14832
14833 self::shoot_ajax_success( $result );
14834 }
14835
14836 /**
14837 * @author Leo Fajardo (@leorw)
14838 * @since 1.2.3
14839 *
14840 * @return array|null
14841 */
14842 function get_affiliate_application_data() {
14843 if ( empty( $this->_storage->affiliate_application_data ) ) {
14844 return null;
14845 }
14846
14847 return $this->_storage->affiliate_application_data;
14848 }
14849
14850 #endregion Affiliation ------------------------------------------------------------
14851
14852 #----------------------------------------------------------------------------------
14853 #region URL Generators
14854 #----------------------------------------------------------------------------------
14855
14856 /**
14857 * Alias to pricing_url().
14858 *
14859 * @author Vova Feldman (@svovaf)
14860 * @since 1.0.2
14861 *
14862 * @uses pricing_url()
14863 *
14864 * @param string $period Billing cycle
14865 * @param bool $is_trial
14866 *
14867 * @return string
14868 */
14869 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14870 return $this->pricing_url( $period, $is_trial );
14871 }
14872
14873 /**
14874 * @author Vova Feldman (@svovaf)
14875 * @since 1.0.9
14876 *
14877 * @uses get_upgrade_url()
14878 *
14879 * @return string
14880 */
14881 function get_trial_url() {
14882 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14883 }
14884
14885 /**
14886 * @author Leo Fajardo (@leorw)
14887 * @since 2.1.4
14888 *
14889 * @param string $new_version
14890 *
14891 * @return string
14892 */
14893 function version_upgrade_checkout_link( $new_version ) {
14894 if ( ! is_object( $this->_license ) ) {
14895 $url = $this->pricing_url();
14896
14897 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14898 } else {
14899 $subscription = $this->_get_subscription( $this->_license->id );
14900
14901 $url = $this->checkout_url(
14902 is_object( $subscription ) ?
14903 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14904 WP_FS__PERIOD_LIFETIME,
14905 false,
14906 array( 'licenses' => $this->_license->quota )
14907 );
14908
14909 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14910 }
14911
14912 return sprintf(
14913 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14914 sprintf(
14915 '<a href="%s">%s</a>',
14916 $this->apply_filters( 'update_notice_checkout_url', $url ),
14917 $purchase_license_text
14918 ),
14919 $new_version
14920 );
14921 }
14922
14923 /**
14924 * Plugin's pricing URL.
14925 *
14926 * @author Vova Feldman (@svovaf)
14927 * @since 1.0.4
14928 *
14929 * @param string $billing_cycle Billing cycle
14930 *
14931 * @param bool $is_trial
14932 *
14933 * @return string
14934 */
14935 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14936 $this->_logger->entrance();
14937
14938 $params = array(
14939 'billing_cycle' => $billing_cycle
14940 );
14941
14942 if ( $is_trial ) {
14943 $params['trial'] = 'true';
14944 }
14945
14946 $url = $this->is_addon() ?
14947 $this->_parent->addon_url( $this->_slug ) :
14948 $this->_get_admin_page_url( 'pricing', $params );
14949
14950 return $this->apply_filters( 'pricing_url', $url );
14951 }
14952
14953 /**
14954 * Checkout page URL.
14955 *
14956 * @author Vova Feldman (@svovaf)
14957 * @since 1.0.6
14958 *
14959 * @param string $billing_cycle Billing cycle
14960 * @param bool $is_trial
14961 * @param array $extra (optional) Extra parameters, override other query params.
14962 * @param bool|null $network
14963 *
14964 * @return string
14965 */
14966 function checkout_url(
14967 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14968 $is_trial = false,
14969 $extra = array(),
14970 $network = null
14971 ) {
14972 $this->_logger->entrance();
14973
14974 $params = array(
14975 'checkout' => 'true',
14976 'billing_cycle' => $billing_cycle,
14977 );
14978
14979 if ( $is_trial ) {
14980 $params['trial'] = 'true';
14981 }
14982
14983 /**
14984 * Params in extra override other params.
14985 */
14986 $params = array_merge( $params, $extra );
14987
14988 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
14989 }
14990
14991 /**
14992 * Add-on checkout URL.
14993 *
14994 * @author Vova Feldman (@svovaf)
14995 * @since 1.1.7
14996 *
14997 * @param number $addon_id
14998 * @param number $pricing_id
14999 * @param string $billing_cycle
15000 * @param bool $is_trial
15001 * @param bool|null $network
15002 *
15003 * @return string
15004 */
15005 function addon_checkout_url(
15006 $addon_id,
15007 $pricing_id,
15008 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15009 $is_trial = false,
15010 $network = null
15011 ) {
15012 return $this->checkout_url( $billing_cycle, $is_trial, array(
15013 'plugin_id' => $addon_id,
15014 'pricing_id' => $pricing_id,
15015 ), $network );
15016 }
15017
15018 #endregion
15019
15020 #endregion ------------------------------------------------------------------
15021
15022 /**
15023 * Check if plugin has any add-ons.
15024 *
15025 * @author Vova Feldman (@svovaf)
15026 * @since 1.0.5
15027 *
15028 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
15029 *
15030 * @return bool
15031 */
15032 function has_addons() {
15033 $this->_logger->entrance();
15034
15035 return $this->_has_addons;
15036 }
15037
15038 /**
15039 * Check if plugin can work in anonymous mode.
15040 *
15041 * @author Vova Feldman (@svovaf)
15042 * @since 1.0.9
15043 *
15044 * @return bool
15045 *
15046 * @deprecated Please use is_enable_anonymous() instead.
15047 */
15048 function enable_anonymous() {
15049 return $this->_enable_anonymous;
15050 }
15051
15052 /**
15053 * Check if plugin can work in anonymous mode.
15054 *
15055 * @author Vova Feldman (@svovaf)
15056 * @since 1.1.9
15057 *
15058 * @return bool
15059 */
15060 function is_enable_anonymous() {
15061 return $this->_enable_anonymous;
15062 }
15063
15064 /**
15065 * Check if plugin is premium only (no free plans).
15066 *
15067 * @author Vova Feldman (@svovaf)
15068 * @since 1.1.9
15069 *
15070 * @return bool
15071 */
15072 function is_only_premium() {
15073 return $this->_is_premium_only;
15074 }
15075
15076 /**
15077 * Checks if the plugin's type is "plugin". The other type is "theme".
15078 *
15079 * @author Leo Fajardo (@leorw)
15080 * @since 1.2.2
15081 *
15082 * @return bool
15083 */
15084 function is_plugin() {
15085 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
15086 }
15087
15088 /**
15089 * @author Leo Fajardo (@leorw)
15090 * @since 1.2.2
15091 *
15092 * @return string
15093 */
15094 function get_module_type() {
15095 if ( ! isset( $this->_module_type ) ) {
15096 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
15097 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
15098 }
15099
15100 return $this->_module_type;
15101 }
15102
15103 /**
15104 * @author Leo Fajardo (@leorw)
15105 * @since 1.2.2
15106 *
15107 * @return string
15108 */
15109 function get_plugin_main_file_path() {
15110 return $this->_plugin_main_file_path;
15111 }
15112
15113 /**
15114 * Check if module has a premium code version.
15115 *
15116 * Serviceware module might be freemium without any
15117 * premium code version, where the paid features
15118 * are all part of the service.
15119 *
15120 * @author Vova Feldman (@svovaf)
15121 * @since 1.2.1.6
15122 *
15123 * @return bool
15124 */
15125 function has_premium_version() {
15126 return $this->_has_premium_version;
15127 }
15128
15129 /**
15130 * Check if feature supported with current site's plan.
15131 *
15132 * @author Vova Feldman (@svovaf)
15133 * @since 1.0.1
15134 *
15135 * @todo IMPLEMENT
15136 *
15137 * @param number $feature_id
15138 *
15139 * @throws Exception
15140 */
15141 function is_feature_supported( $feature_id ) {
15142 throw new Exception( 'not implemented' );
15143 }
15144
15145 /**
15146 * @author Vova Feldman (@svovaf)
15147 * @since 1.0.1
15148 *
15149 * @return bool Is running in SSL/HTTPS
15150 */
15151 function is_ssl() {
15152 return WP_FS__IS_HTTPS;
15153 }
15154
15155 /**
15156 * @author Vova Feldman (@svovaf)
15157 * @since 1.0.9
15158 *
15159 * @return bool Is running in AJAX call.
15160 *
15161 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15162 */
15163 static function is_ajax() {
15164 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15165 }
15166
15167 /**
15168 * Check if it's an AJAX call targeted for the current module.
15169 *
15170 * @author Vova Feldman (@svovaf)
15171 * @since 1.2.0
15172 *
15173 * @param array|string $actions Collection of AJAX actions.
15174 *
15175 * @return bool
15176 */
15177 function is_ajax_action( $actions ) {
15178 // Verify it's an ajax call.
15179 if ( ! self::is_ajax() ) {
15180 return false;
15181 }
15182
15183 // Verify the call is relevant for the plugin.
15184 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15185 return false;
15186 }
15187
15188 // Verify it's one of the specified actions.
15189 if ( is_string( $actions ) ) {
15190 $actions = explode( ',', $actions );
15191 }
15192
15193 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15194 $ajax_action = fs_request_get( 'action' );
15195
15196 foreach ( $actions as $action ) {
15197 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15198 return true;
15199 }
15200 }
15201 }
15202
15203 return false;
15204 }
15205
15206 /**
15207 * Check if it's an AJAX call targeted for current request.
15208 *
15209 * @author Vova Feldman (@svovaf)
15210 * @since 1.2.0
15211 *
15212 * @param array|string $actions Collection of AJAX actions.
15213 * @param number|null $module_id
15214 *
15215 * @return bool
15216 */
15217 static function is_ajax_action_static( $actions, $module_id = null ) {
15218 // Verify it's an ajax call.
15219 if ( ! self::is_ajax() ) {
15220 return false;
15221 }
15222
15223
15224 if ( ! empty( $module_id ) ) {
15225 // Verify the call is relevant for the plugin.
15226 if ( $module_id != fs_request_get( 'module_id' ) ) {
15227 return false;
15228 }
15229 }
15230
15231 // Verify it's one of the specified actions.
15232 if ( is_string( $actions ) ) {
15233 $actions = explode( ',', $actions );
15234 }
15235
15236 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15237 $ajax_action = fs_request_get( 'action' );
15238
15239 foreach ( $actions as $action ) {
15240 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15241 return true;
15242 }
15243 }
15244 }
15245
15246 return false;
15247 }
15248
15249 /**
15250 * @author Vova Feldman (@svovaf)
15251 * @since 1.1.7
15252 *
15253 * @return bool
15254 */
15255 static function is_cron() {
15256 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15257 }
15258
15259 /**
15260 * @author Leo Fajardo (@leorw)
15261 * @since 2.5.0
15262 *
15263 * @return bool
15264 */
15265 static function is_admin_post() {
15266 return ( 'admin-post.php' === self::get_current_page() );
15267 }
15268
15269 /**
15270 * Check if a real user is visiting the admin dashboard.
15271 *
15272 * @author Vova Feldman (@svovaf)
15273 * @since 1.1.7
15274 *
15275 * @return bool
15276 */
15277 function is_user_in_admin() {
15278 return (
15279 is_admin() &&
15280 ! self::is_ajax() &&
15281 ! self::is_cron() &&
15282 ! self::is_admin_post()
15283 );
15284 }
15285
15286 /**
15287 * Check if a real user is in the customizer view.
15288 *
15289 * @author Vova Feldman (@svovaf)
15290 * @since 1.2.2.7
15291 *
15292 * @return bool
15293 */
15294 static function is_customizer() {
15295 return is_customize_preview();
15296 }
15297
15298 /**
15299 * Check if running in HTTPS and if site's plan matching the specified plan.
15300 *
15301 * @param string $plan
15302 * @param bool $exact
15303 *
15304 * @return bool
15305 */
15306 function is_ssl_and_plan( $plan, $exact = false ) {
15307 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15308 }
15309
15310 /**
15311 * Construct plugin's settings page URL.
15312 *
15313 * @author Vova Feldman (@svovaf)
15314 * @since 1.0.4
15315 *
15316 * @param string $page
15317 * @param array $params
15318 * @param bool|null $network
15319 *
15320 * @return string
15321 */
15322 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15323 if ( is_null( $network ) ) {
15324 $network = (
15325 $this->_is_network_active &&
15326 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15327 );
15328 }
15329
15330 if ( 0 < count( $params ) ) {
15331 foreach ( $params as $k => $v ) {
15332 $params[ $k ] = urlencode( $v );
15333 }
15334 }
15335
15336 $page_param = $this->_menu->get_slug( $page );
15337
15338 if ( empty( $page ) &&
15339 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15340 $this->show_opt_in_on_themes_page()
15341 ) {
15342 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15343
15344 return add_query_arg(
15345 $params,
15346 $this->admin_url( 'themes.php', 'admin', $network )
15347 );
15348 }
15349
15350 if ( ! $this->has_settings_menu() ) {
15351 if ( ! empty( $page ) ) {
15352 // Module doesn't have a setting page, but since the request is for
15353 // a specific Freemius page, use the admin.php path.
15354 return add_query_arg( array_merge( $params, array(
15355 'page' => $page_param,
15356 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15357 } else {
15358 if ( $this->is_activation_mode() ) {
15359 /**
15360 * @author Vova Feldman
15361 * @since 1.2.1.6
15362 *
15363 * If plugin doesn't have a settings page, create one for the opt-in screen.
15364 */
15365 return add_query_arg( array_merge( $params, array(
15366 'page' => $this->_slug,
15367 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15368 } else {
15369 // Plugin without a settings page.
15370 return add_query_arg(
15371 $params,
15372 $this->admin_url( 'plugins.php', 'admin', $network )
15373 );
15374 }
15375 }
15376 }
15377
15378 // Module has a submenu settings page.
15379 if ( ! $this->_menu->is_top_level() ) {
15380 $parent_slug = $this->_menu->get_parent_slug();
15381 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15382 $parent_slug :
15383 'admin.php';
15384
15385 return add_query_arg( array_merge( $params, array(
15386 'page' => $page_param,
15387 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15388 }
15389
15390 // Module has a top level CPT settings page.
15391 if ( $this->_menu->is_cpt() ) {
15392 if ( empty( $page ) && $this->is_activation_mode() ) {
15393 return add_query_arg( array_merge( $params, array(
15394 'page' => $page_param
15395 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15396 } else {
15397 if ( ! empty( $page ) ) {
15398 $params['page'] = $page_param;
15399 }
15400
15401 return add_query_arg(
15402 $params,
15403 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15404 );
15405 }
15406 }
15407
15408 // Module has a custom top level settings page.
15409 return add_query_arg( array_merge( $params, array(
15410 'page' => $page_param,
15411 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15412 }
15413
15414 #--------------------------------------------------------------------------------
15415 #region Multisite
15416 #--------------------------------------------------------------------------------
15417
15418 /**
15419 * @author Leo Fajardo (@leorw)
15420 * @since 2.0.0
15421 *
15422 * @return bool
15423 */
15424 function is_network_active() {
15425 return $this->_is_network_active;
15426 }
15427
15428 /**
15429 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15430 *
15431 * @author Leo Fajardo (@leorw)
15432 * @since 2.0.0
15433 *
15434 * @param bool|int[] $all_or_blog_ids
15435 */
15436 private function delegate_connection( $all_or_blog_ids = true ) {
15437 $this->_logger->entrance();
15438
15439 $this->_admin_notices->remove_sticky( 'connect_account' );
15440
15441 if ( true === $all_or_blog_ids ) {
15442 // All sites delegation.
15443 $this->_storage->store( 'is_delegated_connection', true, true );
15444 } else {
15445 // Specified sites delegation.
15446 foreach ( $all_or_blog_ids as $blog_id ) {
15447 $this->delegate_site_connection( $blog_id );
15448 }
15449 }
15450
15451 $this->network_upgrade_mode_completed();
15452 }
15453
15454 /**
15455 * Delegate specific network site conncetion to the site admin.
15456 *
15457 * @author Vova Feldman (@svovaf)
15458 * @since 2.0.0
15459 *
15460 * @param int $blog_id
15461 */
15462 private function delegate_site_connection( $blog_id ) {
15463 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15464 }
15465
15466 /**
15467 * Check if super-admin delegated the connection of ALL sites to the site admins.
15468 *
15469 * @author Vova Feldman (@svovaf)
15470 * @since 2.0.0
15471 *
15472 * @return bool
15473 */
15474 function is_network_delegated_connection() {
15475 if ( ! $this->_is_network_active ) {
15476 return false;
15477 }
15478
15479 return $this->_storage->get( 'is_delegated_connection', false, true );
15480 }
15481
15482 /**
15483 * @author Leo Fajardo (@leorw)
15484 * @since 2.0.0
15485 *
15486 * @param int $blog_id
15487 *
15488 * @return bool
15489 */
15490 function is_site_delegated_connection( $blog_id = 0 ) {
15491 if ( ! $this->_is_network_active ) {
15492 return false;
15493 }
15494
15495 if ( 0 == $blog_id ) {
15496 $blog_id = get_current_blog_id();
15497 }
15498
15499 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15500 }
15501
15502 /**
15503 * Check if delegated the connection. When running within the network admin,
15504 * and haven't specified the blog ID, checks if network level delegated. If running
15505 * within a site admin or specified a blog ID, check if delegated the connection for
15506 * the current context site.
15507 *
15508 * If executed outside the the admin, check if delegated the connection
15509 * for the current context site OR the whole network.
15510 *
15511 * @author Vova Feldman (@svovaf)
15512 * @since 2.0.0
15513 *
15514 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15515 *
15516 * @return bool
15517 */
15518 function is_delegated_connection( $blog_id = 0 ) {
15519 if ( ! $this->_is_network_active ) {
15520 return false;
15521 }
15522
15523 if ( fs_is_network_admin() && 0 == $blog_id ) {
15524 return $this->is_network_delegated_connection();
15525 }
15526
15527 return (
15528 $this->is_network_delegated_connection() ||
15529 $this->is_site_delegated_connection( $blog_id )
15530 );
15531 }
15532
15533 /**
15534 * Check if the current module is active for the site.
15535 *
15536 * @author Vova Feldman (@svovaf)
15537 * @since 2.0.0
15538 *
15539 * @param int $blog_id
15540 *
15541 * @return bool
15542 */
15543 function is_active_for_site( $blog_id ) {
15544 if ( ! is_multisite() ) {
15545 // Not a multisite and this code is executed, means that the plugin is active.
15546 return true;
15547 }
15548
15549 if ( $this->is_theme() ) {
15550 // All themes are site level activated.
15551 return true;
15552 }
15553
15554 if ( $this->_is_network_active ) {
15555 // Plugin was network activated so it's active.
15556 return true;
15557 }
15558
15559 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15560 }
15561
15562 /**
15563 * @todo Implement pagination when accessing the subsites collection.
15564 *
15565 * @author Leo Fajardo (@leorw)
15566 * @since 2.0.0
15567 *
15568 * @param int $limit Default to 1,000
15569 * @param int $offset Default to 0
15570 *
15571 * @return array Active & public sites collection.
15572 */
15573 static function get_sites( $limit = 1000, $offset = 0 ) {
15574 if ( ! is_multisite() ) {
15575 return array();
15576 }
15577
15578 /**
15579 * For consistency with get_blog_list() which only return active public sites.
15580 *
15581 * @author Vova Feldman (@svovaf)
15582 */
15583 $args = array(
15584 /**
15585 * Commented out in order to handle the migration of site options whether the site is public or not.
15586 *
15587 * @author Leo Fajardo (@leorw)
15588 * @since 2.2.1
15589 */
15590 // 'public' => 1,
15591 'archived' => 0,
15592 'mature' => 0,
15593 'spam' => 0,
15594 'deleted' => 0,
15595 'number' => $limit,
15596 'offset' => $offset,
15597 );
15598
15599 return get_sites( $args );
15600 }
15601
15602 /**
15603 * Checks if a given blog is active.
15604 *
15605 * @author Vova Feldman (@svovaf)
15606 * @since 2.0.0
15607 *
15608 * @param $blog_id
15609 *
15610 * @return bool
15611 */
15612 private static function is_site_active( $blog_id ) {
15613 global $wpdb;
15614
15615 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15616
15617 if ( ! is_object( $blog_info ) ) {
15618 return false;
15619 }
15620
15621 return (
15622 true == $blog_info->public &&
15623 false == $blog_info->archived &&
15624 false == $blog_info->mature &&
15625 false == $blog_info->spam &&
15626 false == $blog_info->deleted
15627 );
15628 }
15629
15630 /**
15631 * Get a mapping between the site addresses to their blog IDs.
15632 *
15633 * @author Vova Feldman (@svovaf)
15634 * @since 2.0.0
15635 *
15636 * @return array {
15637 * @key string Site address without protocol with a trailing slash.
15638 * @value int Site's blog ID.
15639 * }
15640 */
15641 private function get_address_to_blog_map() {
15642 $sites = self::get_sites();
15643
15644 // Map site addresses to their blog IDs.
15645 $address_to_blog_map = array();
15646 foreach ( $sites as $site ) {
15647 $blog_id = self::get_site_blog_id( $site );
15648 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15649 $address_to_blog_map[ $address ] = $blog_id;
15650 }
15651
15652 return $address_to_blog_map;
15653 }
15654
15655 /**
15656 * Get a mapping between the site addresses to their blog IDs.
15657 *
15658 * @author Vova Feldman (@svovaf)
15659 * @since 2.0.0
15660 *
15661 * @return array {
15662 * @key int Site's blog ID.
15663 * @value FS_Site Associated install.
15664 * }
15665 */
15666 function get_blog_install_map() {
15667 $sites = self::get_sites();
15668
15669 // Map site blog ID to its install.
15670 $install_map = array();
15671
15672 foreach ( $sites as $site ) {
15673 $blog_id = self::get_site_blog_id( $site );
15674 $install = $this->get_install_by_blog_id( $blog_id );
15675
15676 if ( is_object( $install ) ) {
15677 $install_map[ $blog_id ] = $install;
15678 }
15679 }
15680
15681 return $install_map;
15682 }
15683
15684 /**
15685 * @author Vova Feldman (@svovaf)
15686 * @since 2.5.1
15687 *
15688 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15689 *
15690 * @return int[]
15691 */
15692 private function get_blog_ids( $is_delegated = null ) {
15693 $blog_ids = array();
15694
15695 $sites = self::get_sites();
15696 foreach ( $sites as $site ) {
15697 $blog_id = self::get_site_blog_id( $site );
15698
15699 if (
15700 is_null( $is_delegated ) ||
15701 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15702 ) {
15703 $blog_ids[] = $blog_id;
15704 }
15705 }
15706
15707 return $blog_ids;
15708 }
15709
15710 /**
15711 * @author Vova Feldman (@svovaf)
15712 * @since 2.5.1
15713 *
15714 * @return int[]
15715 */
15716 private function get_non_delegated_blog_ids() {
15717 return $this->get_blog_ids( false );
15718 }
15719
15720 /**
15721 * Gets a map of module IDs that the given user has opted-in to.
15722 *
15723 * @author Leo Fajardo (@leorw)
15724 * @since 2.1.0
15725 *
15726 * @param number $fs_user_id
15727 *
15728 * @return array {
15729 * @key number $plugin_id
15730 * @value bool Always true.
15731 * }
15732 */
15733 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15734 self::$_static_logger->entrance();
15735
15736 if ( ! is_multisite() ) {
15737 $installs = array_merge(
15738 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15739 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15740 );
15741 } else {
15742 $sites = self::get_sites();
15743
15744 $installs = array();
15745 foreach ( $sites as $site ) {
15746 $blog_id = self::get_site_blog_id( $site );
15747
15748 $installs = array_merge(
15749 $installs,
15750 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15751 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15752 );
15753 }
15754 }
15755
15756 $module_ids_map = array();
15757 foreach ( $installs as $install ) {
15758 if ( is_object( $install ) &&
15759 FS_Site::is_valid_id( $install->id ) &&
15760 FS_User::is_valid_id( $install->user_id ) &&
15761 ( $install->user_id == $fs_user_id )
15762 ) {
15763 $module_ids_map[ $install->plugin_id ] = true;
15764 }
15765 }
15766
15767 return $module_ids_map;
15768 }
15769
15770 /**
15771 * @author Leo Fajardo (@leorw)
15772 *
15773 * @return null|array {
15774 * 'install' => FS_Site Module's install,
15775 * 'blog_id' => string The associated blog ID.
15776 * }
15777 */
15778 function find_first_install() {
15779 $sites = self::get_sites();
15780
15781 foreach ( $sites as $site ) {
15782 $blog_id = self::get_site_blog_id( $site );
15783 $install = $this->get_install_by_blog_id( $blog_id );
15784
15785 if ( is_object( $install ) ) {
15786 return array(
15787 'install' => $install,
15788 'blog_id' => $blog_id
15789 );
15790 }
15791 }
15792
15793 return null;
15794 }
15795
15796 /**
15797 * Switches the Freemius site level context to a specified blog.
15798 *
15799 * @author Vova Feldman (@svovaf)
15800 * @since 2.0.0
15801 *
15802 * @param int $blog_id
15803 * @param FS_Site $install
15804 * @param bool $flush
15805 *
15806 * @return bool Since 2.3.1 returns if a switch was made.
15807 */
15808 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
15809 if ( ! is_numeric( $blog_id ) ) {
15810 return false;
15811 }
15812
15813 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15814 return false;
15815 }
15816
15817 switch_to_blog( $blog_id );
15818 $this->_context_is_network_or_blog_id = $blog_id;
15819
15820 self::$_accounts->set_site_blog_context( $blog_id );
15821 $this->_storage->set_site_blog_context( $blog_id );
15822 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15823
15824 $this->_site = is_object( $install ) ?
15825 $install :
15826 $this->get_install_by_blog_id( $blog_id );
15827
15828 $this->_user = false;
15829 $this->_licenses = false;
15830 $this->_license = null;
15831 $this->is_whitelabeled = null;
15832
15833 if ( is_object( $this->_site ) ) {
15834 // Try to fetch user from install.
15835 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15836
15837 if ( ! is_object( $this->_user ) &&
15838 FS_User::is_valid_id( $this->_storage->prev_user_id )
15839 ) {
15840 // Try to fetch previously saved user.
15841 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15842
15843 if ( ! is_object( $this->_user ) ) {
15844 // Fallback to network's user.
15845 $this->_user = $this->get_network_user();
15846 }
15847 }
15848
15849 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15850
15851 if ( ! empty( $all_plugin_licenses ) ) {
15852 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15853 $this->_license = null;
15854 } else {
15855 $license_found = false;
15856 foreach ( $all_plugin_licenses as $license ) {
15857 if ( $license->id == $this->_site->license_id ) {
15858 // License found.
15859 $this->_license = $license;
15860 $license_found = true;
15861 break;
15862 }
15863 }
15864
15865 if ( $license_found ) {
15866 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15867 }
15868 }
15869
15870 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15871 }
15872 }
15873
15874 unset( $this->_site_api );
15875 unset( $this->_user_api );
15876
15877 return true;
15878 }
15879
15880 /**
15881 * Restore the blog context to the blog that originally loaded the module.
15882 *
15883 * @author Vova Feldman (@svovaf)
15884 * @since 2.0.0
15885 */
15886 function restore_current_blog() {
15887 $this->switch_to_blog( $this->_blog_id );
15888 }
15889
15890 /**
15891 * @author Vova Feldman (@svovaf)
15892 * @since 2.0.0
15893 *
15894 * @param array|WP_Site $site
15895 *
15896 * @return int
15897 */
15898 static function get_site_blog_id( &$site ) {
15899 return ( $site instanceof WP_Site ) ?
15900 $site->blog_id :
15901 ( is_object( $site ) && isset( $site->userblog_id ) ?
15902 $site->userblog_id :
15903 $site['blog_id'] );
15904 }
15905
15906 /**
15907 * @author Vova Feldman (@svovaf)
15908 * @since 2.5.1
15909 *
15910 * @param WP_Site[]|array[] $sites
15911 *
15912 * @return int[]
15913 */
15914 static function get_sites_blog_ids( $sites ) {
15915 $blog_ids = array();
15916 foreach ( $sites as $site ) {
15917 $blog_ids[] = self::get_site_blog_id( $site );
15918 }
15919
15920 return $blog_ids;
15921 }
15922
15923 /**
15924 * @author Leo Fajardo (@leorw)
15925 * @since 2.0.0
15926 *
15927 * @param array|WP_Site|null $site
15928 * @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.
15929 *
15930 * @return array
15931 */
15932 function get_site_info( $site = null, $load_registration = false ) {
15933 $this->_logger->entrance();
15934
15935 $switched = false;
15936
15937 $registration_date = null;
15938
15939 if ( is_null( $site ) ) {
15940 $url = self::get_unfiltered_site_url();
15941 $name = get_bloginfo( 'name' );
15942 $blog_id = null;
15943 } else {
15944 $blog_id = self::get_site_blog_id( $site );
15945
15946 if ( get_current_blog_id() != $blog_id ) {
15947 switch_to_blog( $blog_id );
15948 $switched = true;
15949 }
15950
15951 if ( $site instanceof WP_Site ) {
15952 $url = $site->siteurl;
15953 $name = $site->blogname;
15954 $registration_date = $site->registered;
15955 } else {
15956 $url = self::get_unfiltered_site_url( $blog_id );
15957 $name = get_bloginfo( 'name' );
15958 }
15959 }
15960
15961 if ( empty( $registration_date ) && $load_registration ) {
15962 $blog_details = get_blog_details( $blog_id, false );
15963
15964 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15965 $registration_date = $blog_details->registered;
15966 }
15967 }
15968
15969 $info = array(
15970 'uid' => $this->get_anonymous_id( $blog_id ),
15971 'url' => $url,
15972 );
15973
15974 // Add these diagnostic information only if user allowed to track.
15975 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
15976 $info = array_merge( $info, array(
15977 'title' => $name,
15978 'language' => self::get_sanitized_language(),
15979 ) );
15980 }
15981
15982 if ( is_numeric( $blog_id ) ) {
15983 $info['blog_id'] = $blog_id;
15984 }
15985
15986 if ( ! empty( $registration_date ) ) {
15987 $info[ 'registration_date' ] = $registration_date;
15988 }
15989
15990 if ( $switched ) {
15991 restore_current_blog();
15992 }
15993
15994 return $info;
15995 }
15996
15997 /**
15998 * Load the module's install based on the blog ID.
15999 *
16000 * @author Vova Feldman (@svovaf)
16001 * @since 2.0.0
16002 *
16003 * @param int|null $blog_id
16004 *
16005 * @return FS_Site
16006 */
16007 function get_install_by_blog_id( $blog_id = null ) {
16008 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16009 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16010
16011 if ( is_object( $install ) &&
16012 is_numeric( $install->id ) &&
16013 is_numeric( $install->user_id ) &&
16014 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16015 ) {
16016 // Load site.
16017 $install = clone $install;
16018 }
16019
16020 return $install;
16021 }
16022
16023 /**
16024 * Check if module is installed on a specified site.
16025 *
16026 * @author Vova Feldman (@svovaf)
16027 * @since 2.0.0
16028 *
16029 * @param int|null $blog_id
16030 *
16031 * @return bool
16032 */
16033 function is_installed_on_site( $blog_id = null ) {
16034 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16035 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16036
16037 return (
16038 is_object( $install ) &&
16039 is_numeric( $install->id ) &&
16040 is_numeric( $install->user_id ) &&
16041 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16042 );
16043 }
16044
16045 /**
16046 * Check if super-admin connected at least one site via the network opt-in.
16047 *
16048 * @author Vova Feldman (@svovaf)
16049 * @since 2.0.0
16050 *
16051 * @return bool
16052 */
16053 function is_network_registered() {
16054 if ( ! $this->_is_network_active ) {
16055 return false;
16056 }
16057
16058 return FS_User::is_valid_id( $this->_storage->network_user_id );
16059 }
16060
16061 /**
16062 * Returns the main user associated with the network.
16063 *
16064 * @author Vova Feldman (@svovaf)
16065 * @since 2.0.0
16066 *
16067 * @return FS_User
16068 */
16069 function get_network_user() {
16070 if ( ! $this->_is_network_active ) {
16071 return null;
16072 }
16073
16074 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
16075 self::_get_user_by_id( $this->_storage->network_user_id ) :
16076 null;
16077 }
16078
16079 /**
16080 * Returns the current context user or the network's main user.
16081 *
16082 * @author Vova Feldman (@svovaf)
16083 * @since 2.0.0
16084 *
16085 * @return FS_User
16086 */
16087 function get_current_or_network_user() {
16088 return ( $this->_user instanceof FS_User ) ?
16089 $this->_user :
16090 $this->get_network_user();
16091 }
16092
16093 /**
16094 * Returns the main install associated with the network.
16095 *
16096 * @author Vova Feldman (@svovaf)
16097 * @since 2.0.0
16098 *
16099 * @return FS_Site
16100 */
16101 function get_network_install() {
16102 if ( ! $this->_is_network_active ) {
16103 return null;
16104 }
16105
16106 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16107 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
16108 null;
16109 }
16110
16111 /**
16112 * Returns the blog ID that is associated with the main install.
16113 *
16114 * @author Leo Fajardo (@leorw)
16115 * @since 2.0.0
16116 *
16117 * @return int|null
16118 */
16119 function get_network_install_blog_id() {
16120 if ( ! $this->_is_network_active ) {
16121 return null;
16122 }
16123
16124 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16125 $this->_storage->network_install_blog_id :
16126 null;
16127 }
16128
16129 /**
16130 * Returns the current context install or the network's main install.
16131 *
16132 * @author Vova Feldman (@svovaf)
16133 * @since 2.0.0
16134 *
16135 * @return FS_Site
16136 */
16137 function get_current_or_network_install() {
16138 return ( $this->_site instanceof FS_Site ) ?
16139 $this->_site :
16140 $this->get_network_install();
16141 }
16142
16143 /**
16144 * Check if executing a site level action from the network level admin.
16145 *
16146 * @author Vova Feldman (@svovaf)
16147 * @since 2.0.0
16148 *
16149 * @return false|int If yes, return the requested blog ID.
16150 */
16151 private function is_network_level_site_specific_action() {
16152 if ( ! $this->_is_network_active ) {
16153 return false;
16154 }
16155
16156 if ( ! fs_is_network_admin() ) {
16157 return false;
16158 }
16159
16160 $blog_id = fs_request_get( 'blog_id', '' );
16161
16162 return is_numeric( $blog_id ) ? $blog_id : false;
16163 }
16164
16165 /**
16166 * Check if executing an action from the network level admin.
16167 *
16168 * @author Vova Feldman (@svovaf)
16169 * @since 2.0.0
16170 *
16171 * @return bool
16172 */
16173 private function is_network_level_action() {
16174 return ( $this->_is_network_active && fs_is_network_admin() );
16175 }
16176
16177 /**
16178 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16179 * 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.
16180 *
16181 * @author Vova Feldman (@svovaf)
16182 * @since 2.0.0
16183 *
16184 * @param int $context_blog_id
16185 */
16186 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16187 $this->_logger->entrance();
16188
16189 if ( $this->_is_network_active ) {
16190 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16191 $installs_map = $this->get_blog_install_map();
16192
16193 foreach ( $installs_map as $blog_id => $install ) {
16194 /**
16195 * @var FS_Site $install
16196 */
16197 if ( $context_blog_id == $blog_id ) {
16198 continue;
16199 }
16200
16201 if ( $install->user_id != $this->_storage->network_user_id ) {
16202 continue;
16203 }
16204
16205 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16206 $this->_storage->network_install_blog_id = $blog_id;
16207 break;
16208 }
16209 }
16210 }
16211
16212 if ( ! $this->is_registered() ) {
16213 return;
16214 }
16215
16216 if ( $this->is_sync_cron_scheduled() &&
16217 $context_blog_id == $this->get_sync_cron_blog_id()
16218 ) {
16219 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16220 }
16221
16222 if ( $this->is_install_sync_scheduled() &&
16223 $context_blog_id == $this->get_install_sync_cron_blog_id()
16224 ) {
16225 $this->schedule_install_sync( $context_blog_id );
16226 }
16227 }
16228
16229 /**
16230 * Executed after site deactivation, archive, or flag as spam.
16231 *
16232 * @author Vova Feldman (@svovaf)
16233 * @since 2.0.0
16234 *
16235 * @param int $context_blog_id
16236 */
16237 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16238 $this->_logger->entrance();
16239
16240 $install = $this->get_install_by_blog_id( $context_blog_id );
16241
16242 if ( ! is_object( $install ) ) {
16243 // Site not connected.
16244 return;
16245 }
16246
16247 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16248
16249 if ( ! $this->is_registered() ) {
16250 return;
16251 }
16252
16253 $current_blog_id = get_current_blog_id();
16254
16255 $this->switch_to_blog( $context_blog_id );
16256
16257 // Send deactivation event.
16258 $this->sync_install( array(
16259 'is_active' => false,
16260 ) );
16261
16262 $this->switch_to_blog( $current_blog_id );
16263 }
16264
16265 /**
16266 * Executed after site deletion.
16267 *
16268 * @author Vova Feldman (@svovaf)
16269 * @since 2.0.0
16270 *
16271 * @param int $context_blog_id
16272 * @param bool $drop True if site's database tables should be dropped. Default is false.
16273 */
16274 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16275 $this->_logger->entrance();
16276
16277 $install = $this->get_install_by_blog_id( $context_blog_id );
16278
16279 if ( ! is_object( $install ) ) {
16280 // Site not connected.
16281 return;
16282 }
16283
16284 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16285
16286 if ( ! $this->is_registered() ) {
16287 return;
16288 }
16289
16290 $current_blog_id = get_current_blog_id();
16291
16292 $this->switch_to_blog( $context_blog_id );
16293
16294 if ( $drop ) {
16295 // Delete install if dropping site DB.
16296 $this->delete_account_event();
16297 } else {
16298 // Send deactivation event.
16299 $this->sync_install( array(
16300 'is_active' => false,
16301 ) );
16302 }
16303
16304 $this->switch_to_blog( $current_blog_id );
16305 }
16306
16307 /**
16308 * Executed after site deletion, called from wp_delete_site
16309 *
16310 * @author Dario Curvino (@dudo)
16311 * @since 2.5.0
16312 *
16313 * @param WP_Site $old_site
16314 */
16315 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16316 $this->_logger->entrance();
16317
16318 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16319 }
16320
16321 /**
16322 * Executed after site re-activation.
16323 *
16324 * @author Vova Feldman (@svovaf)
16325 * @since 2.0.0
16326 *
16327 * @param int $context_blog_id
16328 */
16329 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16330 $this->_logger->entrance();
16331
16332 $install = $this->get_install_by_blog_id( $context_blog_id );
16333
16334 if ( ! is_object( $install ) ) {
16335 // Site not connected.
16336 return;
16337 }
16338
16339 if ( ! self::is_site_active( $context_blog_id ) ) {
16340 // Site not yet active (can be in spam mode, archived, deleted...).
16341 return;
16342 }
16343
16344 $current_blog_id = get_current_blog_id();
16345
16346 $this->switch_to_blog( $context_blog_id );
16347
16348 // Send re-activation event.
16349 $this->sync_install( array(
16350 'is_active' => true,
16351 ) );
16352
16353 $this->switch_to_blog( $current_blog_id );
16354 }
16355
16356 #endregion Multisite
16357
16358 /**
16359 * @author Leo Fajardo (@leorw)
16360 *
16361 * @param string $path
16362 * @param string $scheme
16363 * @param bool $network
16364 *
16365 * @return string
16366 */
16367 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16368 return ( $this->_is_network_active && $network ) ?
16369 network_admin_url( $path, $scheme ) :
16370 admin_url( $path, $scheme );
16371 }
16372
16373 /**
16374 * Check if currently in a specified admin page.
16375 *
16376 * @author Vova Feldman (@svovaf)
16377 * @since 1.2.2.7
16378 *
16379 * @param string $page
16380 *
16381 * @return bool
16382 */
16383 function is_admin_page( $page ) {
16384 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16385 }
16386
16387 /**
16388 * Check if currently in the product's main admin page.
16389 *
16390 * @author Vova Feldman (@svovaf)
16391 * @since 2.3.1
16392 *
16393 * @return bool
16394 */
16395 function is_main_admin_page() {
16396 return $this->is_admin_page( '' );
16397 }
16398
16399 /**
16400 * Get module's main admin setting page URL.
16401 *
16402 * @author Vova Feldman (@svovaf)
16403 * @since 1.2.2.7
16404 *
16405 * @return string
16406 */
16407 function main_menu_url() {
16408 return $this->_menu->main_menu_url();
16409 }
16410
16411 /**
16412 * Check if currently on the theme's setting page or
16413 * on any of the Freemius added pages (via tabs).
16414 *
16415 * @author Vova Feldman (@svovaf)
16416 * @since 1.2.2.7
16417 *
16418 * @return bool
16419 *
16420 * @deprecated Please use is_product_settings_page() instead;
16421 */
16422 function is_theme_settings_page() {
16423 return $this->is_product_settings_page();
16424 }
16425
16426 /**
16427 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16428 *
16429 * @author Vova Feldman (@svovaf)
16430 * @since 1.2.2.7
16431 *
16432 * @return bool
16433 */
16434 function is_product_settings_page() {
16435 $page = fs_request_get( 'page', '', 'get' );
16436 $menu_slug = $this->_menu->get_slug();
16437
16438 if ( $page === $menu_slug ) {
16439 return true;
16440 }
16441
16442 return fs_starts_with(
16443 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16444 $page,
16445 ( $menu_slug . '-' )
16446 );
16447 }
16448
16449 /**
16450 * Plugin's account page + sync license URL.
16451 *
16452 * @author Vova Feldman (@svovaf)
16453 * @since 1.1.9.1
16454 *
16455 * @param bool|number $plugin_id
16456 * @param bool $add_action_nonce
16457 * @param array $params
16458 *
16459 * @return string
16460 */
16461 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16462 if ( is_numeric( $plugin_id ) ) {
16463 $params['plugin_id'] = $plugin_id;
16464 }
16465
16466 return $this->get_account_url(
16467 $this->get_unique_affix() . '_sync_license',
16468 $params,
16469 $add_action_nonce
16470 );
16471 }
16472
16473 /**
16474 * Plugin's account URL.
16475 *
16476 * @author Vova Feldman (@svovaf)
16477 * @since 1.0.4
16478 *
16479 * @param bool|string $action
16480 * @param array $params
16481 *
16482 * @param bool $add_action_nonce
16483 *
16484 * @return string
16485 */
16486 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16487 if ( is_string( $action ) ) {
16488 $params['fs_action'] = $action;
16489 }
16490
16491 self::require_pluggable_essentials();
16492
16493 return ( $add_action_nonce && is_string( $action ) ) ?
16494 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16495 $this->_get_admin_page_url( 'account', $params );
16496 }
16497
16498 /**
16499 * @author Vova Feldman (@svovaf)
16500 * @since 1.2.0
16501 *
16502 * @param string $tab
16503 * @param bool $action
16504 * @param array $params
16505 * @param bool $add_action_nonce
16506 *
16507 * @return string
16508 *
16509 * @uses get_account_url()
16510 */
16511 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16512 $params['tab'] = $tab;
16513
16514 return $this->get_account_url( $action, $params, $add_action_nonce );
16515 }
16516
16517 /**
16518 * Plugin's account URL.
16519 *
16520 * @author Vova Feldman (@svovaf)
16521 * @since 1.0.4
16522 *
16523 * @param bool|string $topic
16524 * @param bool|string $message
16525 * @param bool|string $summary Since 2.5.1.
16526 *
16527 * @return string
16528 */
16529 function contact_url( $topic = false, $message = false, $summary = false ) {
16530 $params = array();
16531 if ( is_string( $topic ) ) {
16532 $params['topic'] = $topic;
16533 }
16534 if ( is_string( $message ) ) {
16535 $params['message'] = $message;
16536 }
16537
16538 if ( is_string( $summary ) ) {
16539 $params['summary'] = $summary;
16540 }
16541
16542 if ( $this->is_addon() ) {
16543 $params['addon_id'] = $this->get_id();
16544
16545 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16546 } else {
16547 return $this->_get_admin_page_url( 'contact', $params );
16548 }
16549 }
16550
16551 /**
16552 * Add-on direct info URL.
16553 *
16554 * @author Vova Feldman (@svovaf)
16555 * @since 1.1.0
16556 *
16557 * @param string $slug
16558 *
16559 * @return string
16560 */
16561 function addon_url( $slug ) {
16562 return $this->_get_admin_page_url( 'addons', array(
16563 'slug' => $slug
16564 ) );
16565 }
16566
16567 /**
16568 * Add-ons URL.
16569 *
16570 * @author Vova Feldman (@svovaf)
16571 * @since 2.4.5
16572 *
16573 * @return string
16574 */
16575 function get_addons_url() {
16576 return $this->_get_admin_page_url( 'addons' );
16577 }
16578
16579 /* Logger
16580 ------------------------------------------------------------------------------------------------------------------*/
16581 /**
16582 * @param string $id
16583 * @param bool $prefix_slug
16584 *
16585 * @return FS_Logger
16586 */
16587 function get_logger( $id = '', $prefix_slug = true ) {
16588 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16589 }
16590
16591 /**
16592 * Note: This method is used externally so don't delete it.
16593 *
16594 * @param $id
16595 * @param bool $load_options
16596 * @param bool $prefix_slug
16597 *
16598 * @return FS_Option_Manager
16599 */
16600 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16601 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16602 }
16603
16604 /* Security
16605 ------------------------------------------------------------------------------------------------------------------*/
16606 private static function _encrypt( $str ) {
16607 if ( is_null( $str ) ) {
16608 return null;
16609 }
16610
16611 /**
16612 * The encrypt/decrypt functions are used to protect
16613 * the user from messing up with some of the sensitive
16614 * data stored for the module as a JSON in the database.
16615 *
16616 * I used the same suggested hack by the theme review team.
16617 * For more details, look at the function `Base64UrlDecode()`
16618 * in `./sdk/FreemiusBase.php`.
16619 *
16620 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16621 *
16622 * @author Vova Feldman (@svovaf)
16623 * @since 1.2.2
16624 */
16625 $fn = 'base64' . '_encode';
16626
16627 return $fn( $str );
16628 }
16629
16630 static function _decrypt( $str ) {
16631 if ( is_null( $str ) ) {
16632 return null;
16633 }
16634
16635 /**
16636 * The encrypt/decrypt functions are used to protect
16637 * the user from messing up with some of the sensitive
16638 * data stored for the module as a JSON in the database.
16639 *
16640 * I used the same suggested hack by the theme review team.
16641 * For more details, look at the function `Base64UrlDecode()`
16642 * in `./sdk/FreemiusBase.php`.
16643 *
16644 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16645 *
16646 * @author Vova Feldman (@svovaf)
16647 * @since 1.2.2
16648 */
16649 $fn = 'base64' . '_decode';
16650
16651 return $fn( $str );
16652 }
16653
16654 /**
16655 * @author Vova Feldman (@svovaf)
16656 * @since 1.0.5
16657 *
16658 * @param FS_Entity $entity
16659 *
16660 * @return FS_Entity Return an encrypted clone entity.
16661 */
16662 private static function _encrypt_entity( FS_Entity $entity ) {
16663 $clone = clone $entity;
16664 $props = get_object_vars( $entity );
16665
16666 foreach ( $props as $key => $val ) {
16667 $clone->{$key} = self::_encrypt( $val );
16668 }
16669
16670 return $clone;
16671 }
16672
16673 /**
16674 * @author Vova Feldman (@svovaf)
16675 * @since 1.0.5
16676 *
16677 * @param FS_Entity $entity
16678 *
16679 * @return FS_Entity Return an decrypted clone entity.
16680 */
16681 private static function decrypt_entity( FS_Entity $entity ) {
16682 $clone = clone $entity;
16683 $props = get_object_vars( $entity );
16684
16685 foreach ( $props as $key => $val ) {
16686 $clone->{$key} = self::_decrypt( $val );
16687 }
16688
16689 return $clone;
16690 }
16691
16692 /**
16693 * Tries to activate account based on POST params.
16694 *
16695 * @author Vova Feldman (@svovaf)
16696 * @since 1.0.2
16697 *
16698 * @deprecated Not in use, outdated.
16699 */
16700 function _activate_account() {
16701 if ( $this->is_registered() ) {
16702 // Already activated.
16703 return;
16704 }
16705
16706 self::_clean_admin_content_section();
16707
16708 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
16709 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
16710
16711 // Verify matching plugin details.
16712 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
16713 return;
16714 }
16715
16716 $user = new FS_User();
16717 $user->id = fs_request_get( 'user_id' );
16718 $user->public_key = fs_request_get( 'user_public_key' );
16719 $user->secret_key = fs_request_get( 'user_secret_key' );
16720 $user->email = fs_request_get( 'user_email' );
16721 $user->first = fs_request_get( 'user_first' );
16722 $user->last = fs_request_get( 'user_last' );
16723 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
16724
16725 $site = new FS_Site();
16726 $site->id = fs_request_get( 'install_id' );
16727 $site->public_key = fs_request_get( 'install_public_key' );
16728 $site->secret_key = fs_request_get( 'install_secret_key' );
16729 $site->plan_id = fs_request_get( 'plan_id' );
16730
16731 $plans = array();
16732 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
16733 foreach ( $plans_data as $p ) {
16734 $plan = new FS_Plugin_Plan( $p );
16735 if ( $site->plan_id == $plan->id ) {
16736 $plan->title = fs_request_get( 'plan_title' );
16737 $plan->name = fs_request_get( 'plan_name' );
16738 }
16739
16740 $plans[] = $plan;
16741 }
16742
16743 $this->_set_account( $user, $site, $plans );
16744
16745 // Reload the page with the keys.
16746 fs_redirect( $this->_get_admin_page_url() );
16747 }
16748 }
16749
16750 /**
16751 * @author Vova Feldman (@svovaf)
16752 * @since 1.0.7
16753 *
16754 * @param string $email
16755 *
16756 * @return FS_User|false
16757 */
16758 static function _get_user_by_email( $email ) {
16759 self::$_static_logger->entrance();
16760
16761 $email = trim( strtolower( $email ) );
16762
16763 $users = self::get_all_users();
16764
16765 if ( is_array( $users ) ) {
16766 foreach ( $users as $user ) {
16767 if ( $email === trim( strtolower( $user->email ) ) ) {
16768 return $user;
16769 }
16770 }
16771 }
16772
16773 return false;
16774 }
16775
16776 #----------------------------------------------------------------------------------
16777 #region Account (Loading, Updates & Activation)
16778 #----------------------------------------------------------------------------------
16779
16780 /***
16781 * Load account information (user + site).
16782 *
16783 * @author Vova Feldman (@svovaf)
16784 * @since 1.0.1
16785 */
16786 private function _load_account() {
16787 $this->_logger->entrance();
16788
16789 $this->do_action( 'before_account_load' );
16790
16791 $users = self::get_all_users();
16792 $plans = self::get_all_plans( $this->_module_type );
16793
16794 if ( $this->_logger->is_on() && is_admin() ) {
16795 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16796 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16797 }
16798
16799 $site = fs_is_network_admin() ?
16800 $this->get_network_install() :
16801 $this->get_install_by_blog_id();
16802
16803 if ( fs_is_network_admin() &&
16804 $this->is_network_active() &&
16805 ! is_object( $site ) &&
16806 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16807 ) {
16808 $first_install = $this->find_first_install();
16809
16810 if ( is_null( $first_install ) ) {
16811 unset( $this->_storage->network_install_blog_id );
16812 } else {
16813 $site = $first_install['install'];
16814 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16815 }
16816 }
16817
16818 if ( is_object( $site ) &&
16819 is_numeric( $site->id ) &&
16820 is_numeric( $site->user_id ) &&
16821 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16822 ) {
16823 // Load site.
16824 $this->_site = $site;
16825 }
16826
16827 $user = null;
16828 if ( fs_is_network_admin() && $this->_is_network_active ) {
16829 $user = $this->get_network_user();
16830 }
16831
16832 if ( is_object( $user ) ) {
16833 $this->_user = clone $user;
16834 } else if ( $this->_site ) {
16835 $user = self::_get_user_by_id( $this->_site->user_id );
16836
16837 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16838 /**
16839 * Try to load the previous owner. This recovery is used for the following use-case:
16840 * 1. Opt-in
16841 * 2. Cloning site1 to site2
16842 * 3. Ownership switch in site1 (same applies for site2)
16843 * 4. Install data sync on site2
16844 * 5. Now site2's install is associated with the new owner which does not exists locally.
16845 */
16846 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16847 }
16848
16849 if ( ! is_object( $user ) ) {
16850 /**
16851 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16852 */
16853 if (
16854 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16855 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16856 ) {
16857 $user = $this->sync_user_by_current_install();
16858 } else {
16859 return;
16860 }
16861
16862 if ( is_object( $user ) ) {
16863 $this->_storage->user_was_recovered_from_install = true;
16864 } else {
16865 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16866 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16867 1;
16868
16869 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16870 $this->delete_current_install( false );
16871 } else {
16872 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16873
16874 return;
16875 }
16876 }
16877 }
16878
16879 $this->_user = ( $user instanceof FS_User ) ?
16880 clone $user :
16881 null;
16882 }
16883
16884 if ( is_object( $this->_user ) ) {
16885 // Load licenses.
16886 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16887 }
16888
16889 if ( is_object( $this->_site ) ) {
16890 // Load plans.
16891 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16892 $plans[ $this->_slug ] :
16893 array();
16894
16895 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16896 $this->_sync_plans();
16897 } else {
16898 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16899 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16900 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16901 } else {
16902 unset( $this->_plans[ $i ] );
16903 }
16904 }
16905 }
16906
16907 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16908
16909 if ( $this->_site->version != $this->get_plugin_version() ) {
16910 // If stored install version is different than current installed plugin version,
16911 // then update plugin version event.
16912 $this->update_plugin_version_event();
16913 }
16914 }
16915
16916 if ( true === $this->_storage->require_license_activation &&
16917 ! fs_request_get_bool( 'require_license', true )
16918 ) {
16919 $this->_storage->require_license_activation = false;
16920 }
16921
16922 if ( $this->is_theme() ) {
16923 $this->_register_account_hooks();
16924 }
16925
16926 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16927 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16928 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16929 }
16930
16931 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16932 $this->send_pending_clone_update_once();
16933 }
16934 }
16935
16936 /**
16937 * Special user recovery mechanism.
16938 *
16939 * @author Vova Feldman (@svovaf)
16940 * @since 2.0.0
16941 *
16942 * @param number|null $site_user_id
16943 *
16944 * @return \FS_User|mixed
16945 */
16946 private function sync_user_by_current_install( $site_user_id = null ) {
16947 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16948 $site_user_id :
16949 $this->_site->user_id;
16950
16951 $api = $this->get_api_site_scope();
16952
16953 $uid = $this->get_anonymous_id();
16954 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16955
16956 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16957
16958 if ( $this->is_api_result_entity( $result ) ) {
16959 $user = new FS_User( $result );
16960 $this->_user = $user;
16961 $this->_store_user();
16962
16963 return $user;
16964 }
16965
16966 $error_code = FS_Api::get_error_code( $result );
16967
16968 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16969 /**
16970 * Those API errors will continue coming and are not recoverable with the
16971 * current site's data. Therefore, extend the API call's cached result to 7 days.
16972 */
16973 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16974 }
16975
16976 return $result;
16977 }
16978
16979 /**
16980 * @author Vova Feldman (@svovaf)
16981 * @since 1.0.1
16982 *
16983 * @param FS_User $user
16984 * @param FS_Site $site
16985 * @param bool|array $plans
16986 */
16987 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16988 $site->user_id = $user->id;
16989
16990 $this->_site = $site;
16991 $this->_user = $user;
16992 if ( false !== $plans ) {
16993 $this->_plans = $plans;
16994 }
16995
16996 $this->send_install_update();
16997
16998 $this->_store_account();
16999
17000 }
17001
17002 /**
17003 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
17004 * Each version is trimmed after the 16th char.
17005 *
17006 * @author Vova Feldman (@svovaf)
17007 * @since 2.2.1
17008 *
17009 * @return array
17010 */
17011 private function get_versions() {
17012 $versions = array();
17013 $versions['sdk_version'] = $this->version;
17014
17015 // Collect these diagnostic information only if it's allowed.
17016 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17017 $versions['platform_version'] = get_bloginfo( 'version' );
17018 $versions['programming_language_version'] = phpversion();
17019 }
17020
17021 foreach ( $versions as $k => $version ) {
17022 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
17023 }
17024
17025 return $versions;
17026 }
17027
17028 /**
17029 * Get sanitized site language.
17030 *
17031 * @param string $language
17032 * @param int $max_len
17033 *
17034 * @since 2.5.1
17035 * @author Vova Feldman (@svovaf)
17036 *
17037 * @return string
17038 */
17039 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
17040 if ( empty( $language ) ) {
17041 $language = get_bloginfo( 'language' );
17042 }
17043
17044 return substr( $language, 0, $max_len );
17045 }
17046
17047 /**
17048 * Get core version stripped from pre-release and build.
17049 *
17050 * @since 2.5.1
17051 * @author Vova Feldman (@svovaf)
17052 *
17053 * @param string $version
17054 * @param int $parts
17055 * @param int $max_len
17056 * @param bool $include_pre_release
17057 *
17058 * @return string
17059 */
17060 private static function get_core_version(
17061 $version,
17062 $parts = 3,
17063 $max_len = self::VERSION_MAX_CHARS,
17064 $include_pre_release = false
17065 ) {
17066 if ( empty( $version ) ) {
17067 // Version is empty.
17068 return '';
17069 }
17070
17071 if ( is_numeric( $version ) ) {
17072 $is_float_version = is_float( $version );
17073
17074 $version = (string) $version;
17075
17076 /**
17077 * 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.
17078 */
17079 if ( $is_float_version && false === strpos( $version, '.' ) ) {
17080 $version .= '.0';
17081 }
17082 }
17083
17084 if ( ! is_string( $version ) ) {
17085 return '';
17086 }
17087
17088 if ( $parts < 1 ) {
17089 return '';
17090 }
17091
17092 $pre_release_regex = $include_pre_release ?
17093 '(\-(alpha|beta|RC)([0-9]+)?)?' :
17094 '';
17095
17096 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
17097 // Version is not starting with a digit.
17098 return '';
17099 }
17100
17101 return substr( $matches[1], 0, $max_len );
17102 }
17103
17104 /**
17105 * @param string $prop
17106 * @param mixed $val
17107 *
17108 * @return mixed
17109 *@author Vova Feldman (@svovaf)
17110 *
17111 * @since 2.5.1
17112 */
17113 private static function get_api_sanitized_property( $prop, $val ) {
17114 if ( ! is_string( $val ) || empty( $val ) ) {
17115 return $val;
17116 }
17117
17118 switch ( $prop ) {
17119 case 'programming_language_version':
17120 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
17121 return self::get_core_version( $val );
17122 case 'platform_version':
17123 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
17124 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
17125 case 'sdk_version':
17126 // Get the exact SDK version, which can have up to 4 parts.
17127 return self::get_core_version( $val, 4 );
17128 case 'version':
17129 // Get the entire version but just limited in length.
17130 return substr( $val, 0, self::VERSION_MAX_CHARS );
17131 case 'language':
17132 return self::get_sanitized_language( $val );
17133 default:
17134 return $val;
17135 }
17136 }
17137
17138 /**
17139 * @author Leo Fajardo (@leorw)
17140 * @since 2.3.0
17141 *
17142 * @return bool
17143 */
17144 function has_beta_update() {
17145 return (
17146 ! empty( $this->_storage->beta_data ) &&
17147 ( true === $this->_storage->beta_data['is_beta'] ) &&
17148 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
17149 );
17150 }
17151
17152 /**
17153 * @author Leo Fajardo (@leorw)
17154 * @since 2.3.0
17155 *
17156 * @return bool
17157 */
17158 function is_beta() {
17159 return (
17160 ! empty( $this->_storage->beta_data ) &&
17161 ( true === $this->_storage->beta_data['is_beta'] ) &&
17162 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
17163 );
17164 }
17165
17166 /**
17167 * @author Vova Feldman (@svovaf)
17168 * @since 1.1.7.4
17169 *
17170 * @param array $override_with
17171 * @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.
17172 *
17173 * @return array
17174 */
17175 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
17176 $this->_logger->entrance();
17177
17178 $current_user = self::_get_current_wp_user();
17179
17180 $activation_action = $this->get_unique_affix() . '_activate_new';
17181 $return_url = $this->is_anonymous() ?
17182 // If skipped already, then return to the account page.
17183 $this->get_account_url( $activation_action, array(), false ) :
17184 // Return to the module's main page.
17185 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
17186
17187 $versions = $this->get_versions();
17188
17189 $params = array_merge( $versions, array(
17190 'user_firstname' => $current_user->user_firstname,
17191 'user_lastname' => $current_user->user_lastname,
17192 'user_email' => $current_user->user_email,
17193 'plugin_slug' => $this->_slug,
17194 'plugin_id' => $this->get_id(),
17195 'plugin_public_key' => $this->get_public_key(),
17196 'plugin_version' => $this->get_plugin_version(),
17197 'return_url' => fs_nonce_url( $return_url, $activation_action ),
17198 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
17199 'account',
17200 array( 'fs_action' => 'sync_user' )
17201 ), 'sync_user' ),
17202 'is_premium' => $this->is_premium(),
17203 'is_active' => true,
17204 'is_uninstalled' => false,
17205 'is_localhost' => WP_FS__IS_LOCALHOST,
17206 ) );
17207
17208 if ( $this->is_addon() ) {
17209 $parent_fs = $this->get_parent_instance();
17210
17211 $params['parent_plugin_slug'] = $parent_fs->_slug;
17212 $params['parent_plugin_id'] = $parent_fs->get_id();
17213 }
17214
17215 if ( true === $network_level_or_blog_id ) {
17216 if ( ! isset( $override_with['sites'] ) ) {
17217 $params['sites'] = $this->get_sites_for_network_level_optin();
17218 }
17219 } else {
17220 $site = is_numeric( $network_level_or_blog_id ) ?
17221 array( 'blog_id' => $network_level_or_blog_id ) :
17222 null;
17223
17224 $site = $this->get_site_info( $site );
17225
17226 $diagnostic_info = array();
17227 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17228 $diagnostic_info = array(
17229 'site_name' => $site['title'],
17230 'language' => self::get_sanitized_language( $site['language'] ),
17231 );
17232 }
17233
17234 $params = array_merge( $params, $diagnostic_info, array(
17235 'site_uid' => $site['uid'],
17236 'site_url' => $site['url'],
17237 ) );
17238 }
17239
17240 if ( $this->is_pending_activation() &&
17241 ! empty( $this->_storage->pending_license_key )
17242 ) {
17243 $params['license_key'] = $this->_storage->pending_license_key;
17244 }
17245
17246 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17247 // Even though rand() is known for its security issues,
17248 // the timestamp adds another layer of protection.
17249 // It would be very hard for an attacker to get the secret key form here.
17250 // Plus, this should never run in production since the secret should never
17251 // be included in the production version.
17252 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17253 $params['salt'] = md5( uniqid( rand() ) );
17254 $params['secure'] = md5(
17255 $params['ts'] .
17256 $params['salt'] .
17257 $this->get_secret_key()
17258 );
17259 }
17260
17261 if ( is_multisite() && function_exists( 'get_network' ) ) {
17262 $params['network_uid'] = $this->get_anonymous_network_id();
17263 }
17264
17265 return array_merge( $params, $override_with );
17266 }
17267
17268 /**
17269 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17270 * 2. If there was an API error, return the API result.
17271 *
17272 * @author Vova Feldman (@svovaf)
17273 * @since 1.1.7.4
17274 *
17275 * @param string|bool $email
17276 * @param string|bool $first
17277 * @param string|bool $last
17278 * @param string|bool $license_key
17279 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17280 * In this case, the user and site info will be sent to the server but no
17281 * data will be saved to the WP installation's database.
17282 * @param number|bool $trial_plan_id
17283 * @param bool $is_disconnected Whether to opt in without tracking.
17284 * @param null|bool $is_marketing_allowed
17285 * @param array $sites If network-level opt-in, an array of containing details of sites.
17286 * @param bool $redirect
17287 *
17288 * @return string|object
17289 * @use WP_Error
17290 */
17291 function opt_in(
17292 $email = false,
17293 $first = false,
17294 $last = false,
17295 $license_key = false,
17296 $is_uninstall = false,
17297 $trial_plan_id = false,
17298 $is_disconnected = false,
17299 $is_marketing_allowed = null,
17300 $sites = array(),
17301 $redirect = true
17302 ) {
17303 $this->_logger->entrance();
17304
17305 if ( false === $email ) {
17306 $current_user = self::_get_current_wp_user();
17307 $email = $current_user->user_email;
17308 }
17309
17310 /**
17311 * @since 1.2.1 If activating with license key, ignore the context-user
17312 * since the user will be automatically loaded from the license.
17313 */
17314 if ( empty( $license_key ) ) {
17315 // Clean up pending license if opt-ing in again.
17316 $this->_storage->remove( 'pending_license_key' );
17317
17318 if ( ! $is_uninstall ) {
17319 $fs_user = Freemius::_get_user_by_email( $email );
17320 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17321 return $this->install_with_user(
17322 $fs_user,
17323 false,
17324 $trial_plan_id,
17325 $redirect,
17326 true,
17327 $sites
17328 );
17329 }
17330 }
17331 }
17332
17333 $user_info = array();
17334 if ( ! empty( $email ) ) {
17335 $user_info['user_email'] = $email;
17336 }
17337 if ( ! empty( $first ) ) {
17338 $user_info['user_firstname'] = $first;
17339 }
17340 if ( ! empty( $last ) ) {
17341 $user_info['user_lastname'] = $last;
17342 }
17343
17344 if ( ! empty( $sites ) ) {
17345 $is_network = true;
17346
17347 $user_info['sites'] = $sites;
17348 } else {
17349 $is_network = false;
17350 }
17351
17352 $params = $this->get_opt_in_params( $user_info, $is_network );
17353
17354 $filtered_license_key = false;
17355 if ( is_string( $license_key ) ) {
17356 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17357 $params['license_key'] = $filtered_license_key;
17358 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17359 $params['trial_plan_id'] = $trial_plan_id;
17360 }
17361
17362 if ( $is_uninstall ) {
17363 $params['uninstall_params'] = array(
17364 'reason_id' => $this->_storage->uninstall_reason->id,
17365 'reason_info' => $this->_storage->uninstall_reason->info
17366 );
17367 }
17368
17369 if ( isset( $params['license_key'] ) ) {
17370 $fs_user = Freemius::_get_user_by_email( $email );
17371
17372 if ( is_object( $fs_user ) ) {
17373 /**
17374 * If opting in with a context license and the context WP Admin user already opted in
17375 * before from the current site, add the user context security params to avoid the
17376 * unnecessary email activation when the context license is owned by the same context user.
17377 *
17378 * @author Leo Fajardo (@leorw)
17379 * @since 1.2.3
17380 */
17381 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17382 $fs_user,
17383 false,
17384 'install_with_existing_user'
17385 ) );
17386 }
17387 }
17388
17389 if ( is_bool( $is_marketing_allowed ) ) {
17390 $params['is_marketing_allowed'] = $is_marketing_allowed;
17391 }
17392
17393 $params['is_disconnected'] = $is_disconnected;
17394 $params['format'] = 'json';
17395 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17396 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17397
17398 $request = array(
17399 'method' => 'POST',
17400 'body' => $params,
17401 'timeout' => 60,
17402 );
17403
17404 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17405 $response = self::safe_remote_post( $url, $request );
17406
17407 if ( is_wp_error( $response ) ) {
17408 /**
17409 * @var WP_Error $response
17410 */
17411 $result = new stdClass();
17412
17413 $error_code = $response->get_error_code();
17414 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17415
17416 $result->error = (object) array(
17417 'type' => $error_type,
17418 'message' => $response->get_error_message(),
17419 'code' => $error_code,
17420 'http' => 402
17421 );
17422
17423 $this->maybe_modify_api_curl_error_message( $result );
17424
17425 if ( FS_Api::is_blocked( $result ) ) {
17426 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17427 }
17428
17429 $is_connected = null;
17430
17431 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17432 $this->skip_connection( fs_is_network_admin() );
17433
17434 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17435 }
17436
17437 $this->update_connectivity_info( $is_connected );
17438
17439 return $result;
17440 }
17441
17442 $this->update_connectivity_info( true );
17443
17444 // Module is being uninstalled, don't handle the returned data.
17445 if ( $is_uninstall ) {
17446 return true;
17447 }
17448
17449 /**
17450 * 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.
17451 *
17452 * @author Vova Feldman (@svovaf)
17453 * @since 1.2.3
17454 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17455 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17456 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17457 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17458 */
17459 $decoded = is_string( $response['body'] ) ?
17460 json_decode( $response['body'] ) :
17461 null;
17462
17463 if ( empty( $decoded ) ) {
17464 return false;
17465 }
17466
17467 if ( ! $this->is_api_result_object( $decoded ) ) {
17468 if ( ! empty( $params['license_key'] ) ) {
17469 // Pass the fully entered license key to the failure handler.
17470 $params['license_key'] = $license_key;
17471 }
17472
17473 return $is_uninstall ?
17474 $decoded :
17475 $this->apply_filters( 'after_install_failure', $decoded, $params );
17476 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17477 if ( $is_network ) {
17478 $site_ids = array();
17479 foreach ( $sites as $site ) {
17480 $site_ids[] = $site['blog_id'];
17481 }
17482
17483 /**
17484 * Store the sites so that they can be installed once the user has clicked on the activation link
17485 * in the email.
17486 *
17487 * @author Leo Fajardo (@leorw)
17488 */
17489 $this->_storage->pending_sites_info = array(
17490 'blog_ids' => $site_ids,
17491 'license_key' => $license_key,
17492 'trial_plan_id' => $trial_plan_id
17493 );
17494 }
17495
17496 // Pending activation, add message.
17497 return $this->set_pending_confirmation(
17498 ( isset( $decoded->email ) ?
17499 $decoded->email :
17500 true ),
17501 false,
17502 $filtered_license_key,
17503 ! empty( $params['trial_plan_id'] ),
17504 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17505 );
17506 } else if ( isset( $decoded->install_secret_key ) ) {
17507 return $this->install_with_new_user(
17508 $decoded->user_id,
17509 $decoded->user_public_key,
17510 $decoded->user_secret_key,
17511 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17512 $decoded->is_marketing_allowed :
17513 null ),
17514 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17515 $decoded->is_extensions_tracking_allowed :
17516 null ),
17517 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17518 $decoded->is_diagnostic_tracking_allowed :
17519 null ),
17520 $decoded->install_id,
17521 $decoded->install_public_key,
17522 $decoded->install_secret_key,
17523 false
17524 );
17525 } else if ( is_array( $decoded->installs ) ) {
17526 return $this->install_many_with_new_user(
17527 $decoded->user_id,
17528 $decoded->user_public_key,
17529 $decoded->user_secret_key,
17530 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17531 $decoded->is_marketing_allowed :
17532 null ),
17533 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17534 $decoded->is_extensions_tracking_allowed :
17535 null ),
17536 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17537 $decoded->is_diagnostic_tracking_allowed :
17538 null ),
17539 $decoded->installs,
17540 false
17541 );
17542 }
17543
17544 return $decoded;
17545 }
17546
17547 /**
17548 * Set user and site identities.
17549 *
17550 * @author Vova Feldman (@svovaf)
17551 * @since 1.0.9
17552 *
17553 * @param FS_User $user
17554 * @param FS_Site $site
17555 * @param bool $redirect
17556 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17557 * redirect (or return a URL) to the account page with a special parameter to
17558 * trigger the auto installation processes.
17559 *
17560 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17561 */
17562 function setup_account(
17563 FS_User $user,
17564 FS_Site $site,
17565 $redirect = true,
17566 $auto_install = false
17567 ) {
17568 return $this->setup_network_account(
17569 $user,
17570 array( $site ),
17571 $redirect,
17572 $auto_install,
17573 false
17574 );
17575 }
17576
17577 /**
17578 * Set user and site identities.
17579 *
17580 * @author Vova Feldman (@svovaf)
17581 * @since 2.0.0
17582 *
17583 * @param FS_User $user
17584 * @param FS_Site[] $installs
17585 * @param bool $redirect
17586 * @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.
17587 * @param bool $is_network_level_opt_in
17588 *
17589 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17590 */
17591 function setup_network_account(
17592 FS_User $user,
17593 array $installs,
17594 $redirect = true,
17595 $auto_install = false,
17596 $is_network_level_opt_in = true
17597 ) {
17598 $first_install = $installs[0];
17599
17600 $this->_user = $user;
17601 $this->_site = $first_install;
17602
17603 $this->_sync_plans();
17604
17605 if ( $this->_storage->handle_gdpr_admin_notice &&
17606 $this->should_handle_gdpr_admin_notice() &&
17607 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17608 ) {
17609 /**
17610 * Clear user lock after an opt-in.
17611 */
17612 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17613 FS_User_Lock::instance()->unlock();
17614 }
17615
17616 if ( 1 < count( $installs ) ) {
17617 // Only network level opt-in can have more than one install.
17618 $is_network_level_opt_in = true;
17619 }
17620
17621 $this->update_connectivity_info( true );
17622
17623 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17624 // If Freemius was OFF before, turn it on.
17625 $this->turn_on();
17626
17627 $this->handle_account_connection(
17628 $installs,
17629 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17630 );
17631
17632 if ( is_numeric( $first_install->license_id ) ) {
17633 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17634 }
17635
17636 $this->_admin_notices->remove_sticky( 'connect_account' );
17637
17638 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17639 $this->clear_pending_activation_mode();
17640
17641 if ( ! $this->is_paying_or_trial() ) {
17642 $this->_admin_notices->add_sticky(
17643 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17644 'activation_complete'
17645 );
17646 }
17647 }
17648
17649 if ( $this->is_paying_or_trial() ) {
17650 if ( ! $this->is_premium() ||
17651 ! $this->has_premium_version() ||
17652 ! $this->has_settings_menu()
17653 ) {
17654 if ( $this->is_paying() ) {
17655 $this->add_complete_upgrade_instructions_notice(
17656 sprintf(
17657 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17658 $this->get_plan_title()
17659 ),
17660 'plan_upgraded'
17661 );
17662 } else {
17663 $trial_plan = $this->get_trial_plan();
17664
17665 $this->add_complete_upgrade_instructions_notice(
17666 sprintf(
17667 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17668 '<i>' . $this->get_plugin_name() . '</i>'
17669 ),
17670 'trial_started',
17671 $trial_plan->title
17672 );
17673 }
17674 }
17675
17676 $this->_admin_notices->remove_sticky( array(
17677 'trial_promotion',
17678 ) );
17679 }
17680
17681 $plugin_id = fs_request_get( 'plugin_id', false );
17682
17683 // Store activation time ONLY for plugins & themes (not add-ons).
17684 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17685 if ( empty( $this->_storage->activation_timestamp ) ) {
17686 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17687 }
17688 }
17689
17690 $next_page = '';
17691
17692 $extra = array();
17693 if ( $auto_install ) {
17694 $extra['auto_install'] = 'true';
17695 }
17696
17697 if ( is_numeric( $plugin_id ) ) {
17698 /**
17699 * @author Leo Fajardo (@leorw)
17700 * @since 1.2.1.6
17701 *
17702 * Also sync the license after an anonymous user subscribes.
17703 */
17704 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17705 // Add-on was installed - sync license right after install.
17706 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17707 }
17708 } else {
17709 /**
17710 * @author Vova Feldman (@svovaf)
17711 * @since 1.1.9 If site installed with a valid license, sync license.
17712 */
17713 if ( $this->is_paying() ) {
17714 $this->_sync_plugin_license(
17715 true,
17716 // Installs data is already synced in the beginning of this method directly or via _set_account().
17717 false
17718 );
17719 }
17720
17721 // Reload the page with the keys.
17722 $next_page = $this->is_anonymous() ?
17723 // If user previously skipped, redirect to account page.
17724 $this->get_account_url( false, $extra ) :
17725 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17726 }
17727
17728 if ( ! empty( $next_page ) && $redirect ) {
17729 fs_redirect( $next_page );
17730 }
17731
17732 return $next_page;
17733 }
17734
17735 /**
17736 * Install plugin with new user information after approval.
17737 *
17738 * @author Vova Feldman (@svovaf)
17739 * @since 1.0.7
17740 */
17741 function _install_with_new_user() {
17742 $this->_logger->entrance();
17743
17744 if ( $this->is_registered() ) {
17745 return;
17746 }
17747
17748 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17749
17750 $this->update_license_required_permissions_if_anonymous();
17751
17752 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17753 // @todo This logic should be improved because it's executed on every load of a theme.
17754 $this->is_theme()
17755 ) {
17756 // check_admin_referer( $this->_slug . '_activate_new' );
17757
17758 if ( fs_request_has( 'user_secret_key' ) ) {
17759 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17760 $pending_sites_info = $this->_storage->pending_sites_info;
17761
17762 $this->install_many_pending_with_user(
17763 fs_request_get( 'user_id' ),
17764 fs_request_get( 'user_public_key' ),
17765 fs_request_get( 'user_secret_key' ),
17766 fs_request_get_bool( 'is_marketing_allowed', null ),
17767 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17768 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17769 $pending_sites_info['blog_ids'],
17770 $pending_sites_info['license_key'],
17771 $pending_sites_info['trial_plan_id']
17772 );
17773 } else {
17774 $this->install_with_new_user(
17775 fs_request_get( 'user_id' ),
17776 fs_request_get( 'user_public_key' ),
17777 fs_request_get( 'user_secret_key' ),
17778 fs_request_get_bool( 'is_marketing_allowed', null ),
17779 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17780 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17781 fs_request_get( 'install_id' ),
17782 fs_request_get( 'install_public_key' ),
17783 fs_request_get( 'install_secret_key' ),
17784 true,
17785 fs_request_get_bool( 'auto_install' )
17786 );
17787 }
17788 } else if ( $has_pending_activation_confirmation_param ) {
17789 $this->set_pending_confirmation(
17790 fs_request_get( 'user_email' ),
17791 true,
17792 false,
17793 false,
17794 fs_request_get_bool( 'is_suspicious_email' ),
17795 fs_request_get_bool( 'has_upgrade_context' ),
17796 fs_request_get( 'support_email_address' )
17797 );
17798 }
17799 }
17800 }
17801
17802 /**
17803 * @author Vova Feldman (@svovaf)
17804 * @since 2.0.0
17805 *
17806 * @param number $id
17807 * @param string $public_key
17808 * @param string $secret_key
17809 *
17810 * @return \FS_User
17811 */
17812 private function setup_user( $id, $public_key, $secret_key ) {
17813 $user = self::_get_user_by_id( $id );
17814
17815 if ( is_object( $user ) ) {
17816 $this->_user = $user;
17817 } else {
17818 $user = new FS_User();
17819 $user->id = $id;
17820 $user->public_key = $public_key;
17821 $user->secret_key = $secret_key;
17822
17823 $this->_user = $user;
17824 $user_result = $this->get_api_user_scope()->get();
17825 $user = new FS_User( $user_result );
17826
17827 $this->_user = $user;
17828 $this->_store_user();
17829 }
17830
17831 return $user;
17832 }
17833
17834 /**
17835 * Install plugin with new user.
17836 *
17837 * @author Vova Feldman (@svovaf)
17838 * @since 1.1.7.4
17839 *
17840 * @param number $user_id
17841 * @param string $user_public_key
17842 * @param string $user_secret_key
17843 * @param bool|null $is_marketing_allowed
17844 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17845 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17846 * @param number $install_id
17847 * @param string $install_public_key
17848 * @param string $install_secret_key
17849 * @param bool $redirect
17850 * @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.
17851 *
17852 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17853 */
17854 private function install_with_new_user(
17855 $user_id,
17856 $user_public_key,
17857 $user_secret_key,
17858 $is_marketing_allowed,
17859 $is_extensions_tracking_allowed,
17860 $is_diagnostic_tracking_allowed,
17861 $install_id,
17862 $install_public_key,
17863 $install_secret_key,
17864 $redirect = true,
17865 $auto_install = false
17866 ) {
17867 /**
17868 * This method is also executed after opting in with a license key since the
17869 * license can be potentially associated with a different owner.
17870 *
17871 * @since 2.0.0
17872 */
17873 $user = self::_get_user_by_id( $user_id );
17874
17875 if ( ! is_object( $user ) ) {
17876 $user = new FS_User();
17877 $user->id = $user_id;
17878 $user->public_key = $user_public_key;
17879 $user->secret_key = $user_secret_key;
17880
17881 $this->_user = $user;
17882 $user_result = $this->get_api_user_scope()->get();
17883 $user = new FS_User( $user_result );
17884 }
17885
17886 $this->_user = $user;
17887
17888 $site = new FS_Site();
17889 $site->id = $install_id;
17890 $site->public_key = $install_public_key;
17891 $site->secret_key = $install_secret_key;
17892
17893 $this->_site = $site;
17894 $site_result = $this->get_api_site_scope( true )->get();
17895 $site = new FS_Site( $site_result );
17896 $this->_site = $site;
17897
17898 if ( ! is_null( $is_marketing_allowed ) ) {
17899 $this->disable_opt_in_notice_and_lock_user();
17900 }
17901
17902 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17903 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17904 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17905 ) );
17906
17907 return $this->setup_account(
17908 $this->_user,
17909 $this->_site,
17910 $redirect,
17911 $auto_install
17912 );
17913 }
17914
17915 /**
17916 * Install plugin with user.
17917 *
17918 * @author Leo Fajardo (@leorw)
17919 * @since 2.0.0
17920 *
17921 * @param number $user_id
17922 * @param string $user_public_key
17923 * @param string $user_secret_key
17924 * @param bool|null $is_marketing_allowed
17925 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17926 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17927 * @param array $site_ids
17928 * @param bool $license_key
17929 * @param bool $trial_plan_id
17930 * @param bool $redirect
17931 *
17932 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17933 */
17934 private function install_many_pending_with_user(
17935 $user_id,
17936 $user_public_key,
17937 $user_secret_key,
17938 $is_marketing_allowed,
17939 $is_extensions_tracking_allowed,
17940 $is_diagnostic_tracking_allowed,
17941 $site_ids,
17942 $license_key = false,
17943 $trial_plan_id = false,
17944 $redirect = true
17945 ) {
17946 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17947
17948 if ( ! is_null( $is_marketing_allowed ) ) {
17949 $this->disable_opt_in_notice_and_lock_user();
17950 }
17951
17952 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17953 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17954 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17955 ) );
17956
17957 $sites = array();
17958 foreach ( $site_ids as $site_id ) {
17959 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17960 }
17961
17962 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17963 }
17964
17965 /**
17966 * Multi-site install with a new user.
17967 *
17968 * @author Vova Feldman (@svovaf)
17969 * @since 2.0.0
17970 *
17971 * @param number $user_id
17972 * @param string $user_public_key
17973 * @param string $user_secret_key
17974 * @param bool|null $is_marketing_allowed
17975 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17976 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17977 * @param object[] $installs
17978 * @param bool $redirect
17979 * @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.
17980 *
17981 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17982 */
17983 private function install_many_with_new_user(
17984 $user_id,
17985 $user_public_key,
17986 $user_secret_key,
17987 $is_marketing_allowed,
17988 $is_extensions_tracking_allowed,
17989 $is_diagnostic_tracking_allowed,
17990 array $installs,
17991 $redirect = true,
17992 $auto_install = false
17993 ) {
17994 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17995
17996 if ( ! is_null( $is_marketing_allowed ) ) {
17997 $this->disable_opt_in_notice_and_lock_user();
17998 }
17999
18000 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18001 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
18002 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
18003 ) );
18004
18005 $install_ids = array();
18006
18007 foreach ( $installs as $install ) {
18008 $install_ids[] = $install->id;
18009 }
18010
18011 $items_per_request = 25;
18012 $left = count( $install_ids );
18013 $offset = 0;
18014
18015 $installs = array();
18016 while ( $left > 0 ) {
18017 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
18018
18019 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18020 // @todo Handle API error.
18021 }
18022
18023 $installs = array_merge( $installs, $result->installs );
18024
18025 $left -= $items_per_request;
18026 $offset += $items_per_request;
18027 }
18028
18029 foreach ( $installs as &$install ) {
18030 $install = new FS_Site( $install );
18031 }
18032
18033 return $this->setup_network_account(
18034 $this->_user,
18035 $installs,
18036 $redirect,
18037 $auto_install
18038 );
18039 }
18040
18041 /**
18042 * @author Vova Feldman (@svovaf)
18043 * @since 1.1.7.4
18044 *
18045 * @param string|bool $email
18046 * @param bool $redirect
18047 * @param string|bool $license_key Since 1.2.1.5
18048 * @param bool $is_pending_trial Since 1.2.1.5
18049 * @param bool $is_suspicious_email Since 2.5.0
18050 * @param bool $has_upgrade_context Since 2.5.3
18051 * @param bool|string $support_email_address Since 2.5.3
18052 *
18053 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
18054 */
18055 private function set_pending_confirmation(
18056 $email = false,
18057 $redirect = true,
18058 $license_key = false,
18059 $is_pending_trial = false,
18060 $is_suspicious_email = false,
18061 $has_upgrade_context = false,
18062 $support_email_address = false
18063 ) {
18064 $is_network_admin = fs_is_network_admin();
18065
18066 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
18067 /**
18068 * If explicitly asked to ignore pending mode, set to anonymous mode
18069 * 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).
18070 *
18071 * @author Vova Feldman
18072 * @since 1.2.1.6
18073 */
18074 $this->skip_connection( $is_network_admin );
18075 } else {
18076 // Install must be activated via email since
18077 // user with the same email already exist.
18078 $this->_storage->is_pending_activation = true;
18079 $this->_add_pending_activation_notice(
18080 $email,
18081 $is_pending_trial,
18082 $is_suspicious_email,
18083 $has_upgrade_context,
18084 $support_email_address
18085 );
18086 }
18087
18088 if ( ! empty( $license_key ) ) {
18089 $this->_storage->pending_license_key = $license_key;
18090 }
18091
18092 // Remove the opt-in sticky notice.
18093 $this->_admin_notices->remove_sticky( array(
18094 'connect_account',
18095 'trial_promotion',
18096 ) );
18097
18098 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
18099
18100 if ( $redirect ) {
18101 // Reload the page with a pending activation message.
18102 fs_redirect( $next_page );
18103 }
18104
18105 return $next_page;
18106 }
18107
18108 /**
18109 * Install plugin with current logged WP user info.
18110 *
18111 * @author Vova Feldman (@svovaf)
18112 * @since 1.0.7
18113 */
18114 function _install_with_current_user() {
18115 $this->_logger->entrance();
18116
18117 if ( $this->is_registered() ) {
18118 return;
18119 }
18120
18121 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
18122 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
18123
18124 /**
18125 * @author Vova Feldman (@svovaf)
18126 * @since 1.1.9 Add license key if given.
18127 */
18128 $license_key = fs_request_get( 'license_secret_key' );
18129
18130 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18131 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
18132 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
18133 ) );
18134
18135 $this->install_with_current_user( $license_key );
18136 }
18137 }
18138
18139
18140 /**
18141 * @author Vova Feldman (@svovaf)
18142 * @since 1.1.7.4
18143 *
18144 * @param string|bool $license_key
18145 * @param number|bool $trial_plan_id
18146 * @param array $sites Since 2.0.0
18147 * @param bool $redirect
18148 *
18149 * @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.
18150 */
18151 function install_with_current_user(
18152 $license_key = false,
18153 $trial_plan_id = false,
18154 $sites = array(),
18155 $redirect = true
18156 ) {
18157 // Get current logged WP user.
18158 $current_user = self::_get_current_wp_user();
18159
18160 // Find the relevant FS user by the email.
18161 $user = self::_get_user_by_email( $current_user->user_email );
18162
18163 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18164 }
18165
18166 /**
18167 * @author Vova Feldman (@svovaf)
18168 * @since 2.0.0
18169 *
18170 * @param \FS_User $user
18171 * @param string|bool $license_key
18172 * @param number|bool $trial_plan_id
18173 * @param bool $redirect
18174 * @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.
18175 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18176 *
18177 * @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.
18178 */
18179 function install_with_user(
18180 FS_User $user,
18181 $license_key = false,
18182 $trial_plan_id = false,
18183 $redirect = true,
18184 $setup_account = true,
18185 $sites = array()
18186 ) {
18187 // We have to set the user before getting user scope API handler.
18188 $this->_user = $user;
18189
18190 // Install the plugin.
18191 $result = $this->create_installs_with_user(
18192 $user,
18193 $license_key,
18194 $trial_plan_id,
18195 $sites,
18196 $redirect
18197 );
18198
18199 if ( ! $this->is_api_result_entity( $result ) &&
18200 ! $this->is_api_result_object( $result, 'installs' )
18201 ) {
18202 // @todo Handler potential API error of the $result
18203 }
18204
18205 if ( empty( $sites ) ) {
18206 $site = new FS_Site( $result );
18207 $this->_site = $site;
18208
18209 if ( ! $setup_account ) {
18210 $this->_store_site();
18211
18212 $this->sync_plan_if_not_exist( $site->plan_id );
18213
18214 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18215 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18216 }
18217
18218 $this->_admin_notices->remove_sticky( 'connect_account', false );
18219
18220 return $site;
18221 }
18222
18223 return $this->setup_account( $this->_user, $this->_site, $redirect );
18224 } else {
18225 $installs = array();
18226 foreach ( $result->installs as $install ) {
18227 $installs[] = new FS_Site( $install );
18228 }
18229
18230 return $this->setup_network_account(
18231 $user,
18232 $installs,
18233 $redirect
18234 );
18235 }
18236 }
18237
18238 /**
18239 * Initiate an API request to create a collection of installs.
18240 *
18241 * @author Vova Feldman (@svovaf)
18242 * @since 2.0.0
18243 *
18244 * @param \FS_User $user
18245 * @param bool $license_key
18246 * @param bool $trial_plan_id
18247 * @param array $sites
18248 * @param bool $redirect
18249 * @param bool $silent
18250 *
18251 * @return object|mixed
18252 */
18253 private function create_installs_with_user(
18254 FS_User $user,
18255 $license_key = false,
18256 $trial_plan_id = false,
18257 $sites = array(),
18258 $redirect = false,
18259 $silent = false
18260 ) {
18261 $extra_install_params = array(
18262 'uid' => $this->get_anonymous_id(),
18263 'is_disconnected' => false,
18264 );
18265
18266 if ( ! empty( $license_key ) ) {
18267 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18268
18269 if ( $silent ) {
18270 $extra_install_params['ignore_license_owner'] = true;
18271 }
18272 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18273 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18274 }
18275
18276 if ( ! empty( $sites ) ) {
18277 $extra_install_params['sites'] = $sites;
18278 }
18279
18280 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18281
18282 // Install the plugin.
18283 $result = $this->get_api_user_scope_by_user( $user )->call(
18284 "/plugins/{$this->get_id()}/installs.json",
18285 'post',
18286 $args
18287 );
18288
18289 if ( ! $this->is_api_result_entity( $result ) &&
18290 ! $this->is_api_result_object( $result, 'installs' )
18291 ) {
18292 if ( ! empty( $args['license_key'] ) ) {
18293 // Pass the fully entered license key to the failure handler.
18294 $args['license_key'] = $license_key;
18295 }
18296
18297 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18298
18299 if ( ! $silent ) {
18300 $this->_admin_notices->add(
18301 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18302 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18303 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18304 'error'
18305 );
18306 }
18307
18308 if ( $redirect ) {
18309 /**
18310 * We set the user before getting the user scope API handler, so the user became temporarily
18311 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18312 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18313 * activation page based on the return value of `is_registered()`. In addition, in case the
18314 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18315 * misleading plugin activation errors will be shown on the `Plugins` page.
18316 *
18317 * @author Leo Fajardo (@leorw)
18318 */
18319 $this->_user = null;
18320
18321 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18322 }
18323 }
18324
18325 return $result;
18326 }
18327
18328 /**
18329 * Tries to activate add-on account based on parent plugin info.
18330 *
18331 * @author Vova Feldman (@svovaf)
18332 * @since 1.0.6
18333 *
18334 * @param Freemius $parent_fs
18335 * @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.
18336 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18337 */
18338 private function _activate_addon_account(
18339 Freemius $parent_fs,
18340 $network_level_or_blog_id = null,
18341 FS_Plugin_License $bundle_license = null
18342 ) {
18343 if ( $this->is_registered() ) {
18344 // Already activated.
18345 return;
18346 }
18347
18348 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18349 $permissions = array();
18350 foreach ( $permission_ids as $permission_id ) {
18351 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18352 }
18353
18354 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18355
18356 /**
18357 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18358 * already returns the data for the current blog.
18359 *
18360 * @author Leo Fajardo (@leorw)
18361 * @since 2.3.0
18362 */
18363 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18364 array() :
18365 array( 'uid' => $this->get_anonymous_id() );
18366
18367 $params = $this->get_install_data_for_api(
18368 $uid_param_to_override,
18369 false,
18370 false,
18371 /**
18372 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18373 * already includes the data for it.
18374 *
18375 * @author Leo Fajardo (@leorw)
18376 * @since 2.3.0
18377 */
18378 ( true !== $network_level_or_blog_id )
18379 );
18380
18381 if ( true === $network_level_or_blog_id ) {
18382 $params['sites'] = $this->get_sites_for_network_level_optin();
18383
18384 if ( empty( $params['sites'] ) ) {
18385 return;
18386 }
18387 }
18388
18389 if ( is_object( $bundle_license ) ) {
18390 $params['license_key'] = $bundle_license->secret_key;
18391 }
18392
18393 // Activate add-on with parent plugin credentials.
18394 $result = $parent_fs->get_api_site_scope()->call(
18395 "/addons/{$this->_plugin->id}/installs.json",
18396 'post',
18397 $params
18398 );
18399
18400 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18401 if ( is_object( $bundle_license ) ) {
18402 /**
18403 * 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.
18404 *
18405 * @author Leo Fajardo (@leorw)
18406 * @since 2.4.0
18407 */
18408 } else {
18409 $error_message = FS_Api::is_api_error_object( $result ) ?
18410 $result->error->message :
18411 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18412
18413 $this->_admin_notices->add(
18414 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18415 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18416 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18417 'error'
18418 );
18419 }
18420
18421 return;
18422 }
18423
18424 $addon_installs = $result->installs;
18425 foreach ( $addon_installs as $key => $addon_install ) {
18426 $addon_installs[ $key ] = new FS_Site( $addon_install );
18427 }
18428
18429 $first_install = $addon_installs[0];
18430
18431 // Get user information based on parent's plugin.
18432 $user = $parent_fs->get_user();
18433
18434 // First of all, set site and user info - otherwise we won't
18435 // be able to invoke API calls.
18436 $this->_site = $first_install;
18437 $this->_user = $user;
18438
18439 // Sync add-on plans.
18440 $this->_sync_plans();
18441
18442 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18443
18444 // Get site's current plan.
18445 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18446
18447 // Sync licenses.
18448 $this->_sync_licenses();
18449
18450 if ( ! fs_is_network_admin() ) {
18451 // Try to activate premium license.
18452 $this->_activate_license( true, $bundle_license );
18453
18454 if ( is_object( $bundle_license ) ) {
18455 $this->maybe_activate_bundle_license( $bundle_license );
18456 }
18457 } else {
18458 if ( is_object( $bundle_license ) ) {
18459 $premium_license = $bundle_license;
18460 } else {
18461 $license_id = fs_request_get( 'license_id' );
18462
18463 if ( is_object( $this->_site ) &&
18464 FS_Plugin_License::is_valid_id( $license_id ) &&
18465 $license_id == $this->_site->license_id
18466 ) {
18467 // License is already activated.
18468 return;
18469 }
18470
18471 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18472 $this->_get_license_by_id( $license_id ) :
18473 $this->_get_available_premium_license();
18474 }
18475
18476 if ( is_object( $premium_license ) ) {
18477 $this->maybe_network_activate_addon_license( $premium_license );
18478 }
18479 }
18480 }
18481
18482 /**
18483 * @author Leo Fajardo (@leorw)
18484 * @since 2.3.0
18485 *
18486 * @param FS_Site[] $installs
18487 * @param bool $is_site_level
18488 */
18489 private function handle_account_connection( $installs, $is_site_level ) {
18490 $first_install = $installs[0];
18491
18492 if ( $is_site_level ) {
18493 $this->_set_account( $this->_user, $first_install );
18494
18495 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18496 } else {
18497 $this->_store_user();
18498
18499 // Map site addresses to their blog IDs.
18500 $address_to_blog_map = $this->get_address_to_blog_map();
18501
18502 $first_blog_id = null;
18503 $blog_2_install_map = array();
18504 foreach ( $installs as $install ) {
18505 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18506 $blog_id = $address_to_blog_map[ $address ];
18507
18508 $this->_store_site( true, $blog_id, $install );
18509
18510 if ( is_null( $first_blog_id ) ) {
18511 $first_blog_id = $blog_id;
18512 }
18513
18514 $blog_2_install_map[ $blog_id ] = $install;
18515 }
18516
18517 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18518 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18519 ) {
18520 // Store network user.
18521 $this->_storage->network_user_id = $this->_user->id;
18522 }
18523
18524 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18525 $this->_storage->network_install_blog_id = $first_blog_id;
18526 }
18527
18528 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18529 // Super admin opted in for all sites in the network.
18530 $this->_storage->is_network_connected = true;
18531 }
18532
18533 $this->_store_licenses( false );
18534
18535 self::$_accounts->store();
18536
18537 // Don't sync the installs data on network upgrade
18538 if ( ! $this->network_upgrade_mode_completed() ) {
18539 $this->send_installs_update();
18540 }
18541
18542 $current_blog = get_current_blog_id();
18543
18544 foreach ( $blog_2_install_map as $blog_id => $install ) {
18545 $this->switch_to_blog( $blog_id );
18546
18547 $this->do_action( 'after_account_connection', $this->_user, $install );
18548 }
18549
18550 // Switch install context back to the first install.
18551 $this->switch_to_blog(
18552 $current_blog,
18553 $first_install,
18554 ( $this->_site->id != $first_install->id )
18555 );
18556
18557 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18558 }
18559 }
18560
18561 /**
18562 * Tries to activate parent account based on add-on's info.
18563 *
18564 * @author Vova Feldman (@svovaf)
18565 * @since 1.2.2.7
18566 *
18567 * @param Freemius $parent_fs
18568 */
18569 private function activate_parent_account( Freemius $parent_fs ) {
18570 if ( ! $this->is_addon() ) {
18571 // This is not an add-on.
18572 return;
18573 }
18574
18575 if ( $parent_fs->is_registered() ) {
18576 // Already activated.
18577 return;
18578 }
18579
18580 // Activate parent with add-on's user credentials.
18581 $parent_install = $this->get_api_user_scope()->call(
18582 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18583 'post',
18584 $parent_fs->get_install_data_for_api( array(
18585 'uid' => $parent_fs->get_anonymous_id(),
18586 ), false, false )
18587 );
18588
18589 if ( isset( $parent_install->error ) ) {
18590 $this->_admin_notices->add(
18591 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18592 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18593 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18594 'error'
18595 );
18596
18597 return;
18598 }
18599
18600 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18601
18602 if ( $parent_fs->is_pending_activation() ) {
18603 $parent_fs->clear_pending_activation_mode();
18604 }
18605
18606 // Get user information based on parent's plugin.
18607 $user = $this->get_user();
18608
18609 // First of all, set site info - otherwise we won't
18610 // be able to invoke API calls.
18611 $parent_fs->_site = new FS_Site( $parent_install );
18612 $parent_fs->_user = $user;
18613
18614 // Sync add-on plans.
18615 $parent_fs->_sync_plans();
18616
18617 $parent_fs->update_license_required_permissions_if_anonymous();
18618
18619 $parent_fs->_set_account( $user, $parent_fs->_site );
18620 }
18621
18622 #endregion
18623
18624 #----------------------------------------------------------------------------------
18625 #region Admin Menu Items
18626 #----------------------------------------------------------------------------------
18627
18628 private $_menu_items = array();
18629
18630 /**
18631 * @author Vova Feldman (@svovaf)
18632 * @since 1.2.1.8
18633 *
18634 * @return array
18635 */
18636 function get_menu_items() {
18637 return $this->_menu_items;
18638 }
18639
18640 /**
18641 * @author Vova Feldman (@svovaf)
18642 * @since 1.0.7
18643 *
18644 * @return string
18645 */
18646 function get_menu_slug() {
18647 return $this->_menu->get_slug();
18648 }
18649
18650 /**
18651 * @author Vova Feldman (@svovaf)
18652 * @since 1.0.9
18653 */
18654 function _prepare_admin_menu() {
18655 // if ( ! $this->is_on() ) {
18656 // return;
18657 // }
18658
18659 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18660 return;
18661 }
18662
18663 /**
18664 * When running from a site admin with a network activated module and the connection
18665 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18666 * site level settings.
18667 *
18668 * @author Vova Feldman (@svovaf)
18669 * @since 2.0.0
18670 */
18671 $should_hide_site_admin_settings = (
18672 $this->_is_network_active &&
18673 ! fs_is_network_admin() &&
18674 ! $this->is_delegated_connection() &&
18675 ! $this->is_anonymous() &&
18676 ! $this->is_registered()
18677 );
18678
18679 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18680
18681 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18682 $should_hide_site_admin_settings
18683 ) {
18684 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18685 } else {
18686 $this->do_action( fs_is_network_admin() ?
18687 'before_network_admin_menu_init' :
18688 'before_admin_menu_init'
18689 );
18690
18691 $this->add_menu_action();
18692
18693 $this->add_network_menu_when_missing();
18694
18695 $this->add_submenu_items();
18696 }
18697 }
18698
18699 /**
18700 * Admin dashboard menu items modifications.
18701 *
18702 * NOTE: admin_menu action executed before admin_init.
18703 *
18704 * @author Vova Feldman (@svovaf)
18705 * @since 1.0.7
18706 *
18707 */
18708 private function add_menu_action() {
18709 if ( $this->is_activation_mode() ) {
18710 if ( $this->show_opt_in_on_setting_page() ) {
18711 $this->override_plugin_menu_with_activation();
18712 } else {
18713 /**
18714 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18715 */
18716 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18717 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18718 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18719 fs_request_get_bool( 'pending_activation' )
18720 ) {
18721 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18722 }
18723 }
18724 } else {
18725 if ( ! $this->is_registered() ) {
18726 // If not registered try to install user.
18727 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18728 $this->_install_with_new_user();
18729 }
18730 } else if (
18731 fs_request_is_action( 'sync_user' ) &&
18732 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18733 ) {
18734 $this->_handle_account_user_sync();
18735 }
18736 }
18737 }
18738
18739 /**
18740 * @author Vova Feldman (@svovaf)
18741 * @since 1.0.1
18742 */
18743 function _redirect_on_clicked_menu_link() {
18744 $this->_logger->entrance();
18745
18746 $page = fs_request_get('page');
18747 $page = is_string($page) ? strtolower($page) : '';
18748
18749 $this->_logger->log( 'page = ' . $page );
18750
18751 foreach ( $this->_menu_items as $priority => $items ) {
18752 foreach ( $items as $item ) {
18753 if ( isset( $item['url'] ) ) {
18754 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18755 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18756
18757 fs_redirect( $item['url'] );
18758 }
18759 }
18760 }
18761 }
18762 }
18763
18764 /**
18765 * Remove plugin's all admin menu items & pages, and replace with activation page.
18766 *
18767 * @author Vova Feldman (@svovaf)
18768 * @since 1.0.1
18769 */
18770 private function override_plugin_menu_with_activation() {
18771 $this->_logger->entrance();
18772
18773 $hook = false;
18774
18775 if ( ! $this->has_settings_menu() ) {
18776 // Add the opt-in page without a menu item.
18777 $hook = FS_Admin_Menu_Manager::add_subpage(
18778 '',
18779 $this->get_plugin_name(),
18780 $this->get_plugin_name(),
18781 'manage_options',
18782 $this->_slug,
18783 array( &$this, '_connect_page_render' )
18784 );
18785 } else if ( $this->_menu->is_top_level() ) {
18786 if ( $this->_menu->is_override_exact() ) {
18787 // Make sure the current page is matching the activation page.
18788 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18789 return;
18790 }
18791 }
18792
18793 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18794
18795 if ( false === $hook ) {
18796 // Create new menu item just for the opt-in.
18797 $hook = FS_Admin_Menu_Manager::add_page(
18798 $this->get_plugin_name(),
18799 $this->get_plugin_name(),
18800 'manage_options',
18801 $this->_menu->get_slug(),
18802 array( &$this, '_connect_page_render' )
18803 );
18804 }
18805 } else {
18806 $menus = array( $this->_menu->get_parent_slug() );
18807
18808 if ( $this->_menu->is_override_exact() ) {
18809 // Make sure the current page is matching the activation page.
18810 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18811 return;
18812 }
18813 }
18814
18815 foreach ( $menus as $parent_slug ) {
18816 $hook = $this->_menu->override_submenu_action(
18817 $parent_slug,
18818 $this->_menu->get_raw_slug(),
18819 array( &$this, '_connect_page_render' )
18820 );
18821
18822 if ( false !== $hook ) {
18823 // Found plugin's submenu item.
18824 break;
18825 }
18826 }
18827 }
18828
18829 if ( $this->is_activation_page() ) {
18830 // Clean admin page from distracting content.
18831 self::_clean_admin_content_section();
18832 }
18833
18834 if ( false !== $hook ) {
18835 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18836 $this->_install_with_current_user();
18837 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18838 $this->_install_with_new_user();
18839 }
18840 }
18841 }
18842
18843 /**
18844 * If a plugin was network activated and connected but don't have a network
18845 * level settings, then add an artificial menu item for the Account and other
18846 * Freemius settings.
18847 *
18848 * @author Vova Feldman (@svovaf)
18849 * @since 2.0.0
18850 */
18851 private function add_network_menu_when_missing() {
18852 $this->_logger->entrance();
18853
18854 if ( ! $this->_is_network_active ) {
18855 // Plugin wasn't activated on the network level.
18856 return;
18857 }
18858
18859 if ( ! fs_is_network_admin() ) {
18860 // The context is not the network admin.
18861 return;
18862 }
18863
18864 if ( $this->_menu->has_network_menu() ) {
18865 // Plugin already has a network level menu.
18866 return;
18867 }
18868
18869 if ( $this->is_network_activation_mode() ) {
18870 /**
18871 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18872 * screen is being shown.
18873 *
18874 * @author Leo Fajardo (@leorw)
18875 */
18876 return;
18877 }
18878
18879 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18880 if ( $this->is_network_delegated_connection() ) {
18881 // Super-admin delegated the connection to the site admins.
18882 return;
18883 }
18884 }
18885
18886 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18887
18888 if ( $this->_menu->has_menu() ||
18889 ! $this->is_addon() ||
18890 $this->is_activation_mode()
18891 ) {
18892 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18893 $this->get_plugin_name(),
18894 $this->get_plugin_name(),
18895 'manage_options',
18896 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18897 );
18898 }
18899 } else {
18900 $this->_menu->add_subpage_and_update(
18901 $this->_menu->get_parent_slug(),
18902 $this->get_plugin_name(),
18903 $this->get_plugin_name(),
18904 'manage_options',
18905 $this->_menu->get_slug()
18906 );
18907 }
18908 }
18909
18910 /**
18911 * @author Leo Fajardo (@leorw)
18912 * @since 1.2.1
18913 *
18914 * return string
18915 */
18916 function get_top_level_menu_capability() {
18917 global $menu;
18918
18919 $top_level_menu_slug = $this->get_top_level_menu_slug();
18920
18921 foreach ( $menu as $menu_info ) {
18922 /**
18923 * The second element in the menu info array is the capability/role that has access to the menu and the
18924 * third element is the menu slug.
18925 */
18926 if ( $menu_info[2] === $top_level_menu_slug ) {
18927 return $menu_info[1];
18928 }
18929 }
18930
18931 return 'read';
18932 }
18933
18934 /**
18935 * @author Vova Feldman (@svovaf)
18936 * @since 1.0.0
18937 *
18938 * @return string
18939 */
18940 private function get_top_level_menu_slug() {
18941 return ( $this->is_addon() ?
18942 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18943 $this->_menu->get_top_level_menu_slug() );
18944 }
18945
18946 /**
18947 * @author Vova Feldman (@svovaf)
18948 * @since 1.2.2.7
18949 *
18950 * @return string
18951 */
18952 function get_pricing_cta_label() {
18953 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18954
18955 if ( $this->is_in_trial_promotion() &&
18956 ! $this->is_paying_or_trial()
18957 ) {
18958 // If running a trial promotion, modify the pricing to load the trial.
18959 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18960 } else if ( $this->is_paying() ) {
18961 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18962 }
18963
18964 return $label;
18965 }
18966
18967 /**
18968 * @author Vova Feldman (@svovaf)
18969 * @since 1.2.2.7
18970 *
18971 * @return bool
18972 */
18973 function is_pricing_page_visible() {
18974 return (
18975 // Has at least one paid plan.
18976 $this->has_paid_plan() &&
18977 // Didn't ask to hide the pricing page.
18978 $this->is_page_visible( 'pricing' ) &&
18979 // Don't have a valid active license or has more than one plan.
18980 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18981 );
18982 }
18983
18984 /**
18985 * @author Leo Fajardo (@leorw)
18986 * @since 2.3.0
18987 *
18988 * @param bool $is_activation_mode
18989 *
18990 * @return bool
18991 */
18992 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18993 if ( $this->is_addon() ) {
18994 // No submenu items or action links for add-ons.
18995 return false;
18996 }
18997
18998 if ( $this->show_opt_in_on_themes_page() ) {
18999 if ( ! fs_is_network_admin() ) {
19000 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
19001 return true;
19002 }
19003 } else if ( $is_activation_mode ) {
19004 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
19005 return false;
19006 }
19007
19008 if ( fs_is_network_admin() ) {
19009 /**
19010 * Add submenu items or action links to network level when plugin was network activated and the super
19011 * admin did NOT delegate the connection of all sites to site admins.
19012 */
19013 return (
19014 $this->_is_network_active &&
19015 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
19016 ! $this->is_network_delegated_connection() )
19017 );
19018 }
19019
19020 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
19021 }
19022
19023 /**
19024 * Add default Freemius menu items.
19025 *
19026 * @author Vova Feldman (@svovaf)
19027 * @since 1.0.0
19028 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
19029 */
19030 private function add_submenu_items() {
19031 $this->_logger->entrance();
19032
19033 $is_activation_mode = $this->is_activation_mode();
19034
19035 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
19036
19037 if ( $add_submenu_items ) {
19038 if ( $this->has_affiliate_program() ) {
19039 // Add affiliation page.
19040 $this->add_submenu_item(
19041 $this->get_text_inline( 'Affiliation', 'affiliation' ),
19042 array( &$this, '_affiliation_page_render' ),
19043 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
19044 'manage_options',
19045 'affiliation',
19046 'Freemius::_clean_admin_content_section',
19047 WP_FS__DEFAULT_PRIORITY,
19048 $this->is_submenu_item_visible( 'affiliation' )
19049 );
19050 }
19051 }
19052
19053 if ( $add_submenu_items ||
19054 ( $is_activation_mode &&
19055 $this->is_only_premium() &&
19056 $this->is_admin_page( 'account' ) &&
19057 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
19058 )
19059 ) {
19060 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
19061 $show_account = (
19062 $this->is_submenu_item_visible( 'account' ) &&
19063 /**
19064 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
19065 */
19066 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
19067 );
19068
19069 // Add user account page.
19070 $this->add_submenu_item(
19071 $this->get_text_inline( 'Account', 'account' ),
19072 array( &$this, '_account_page_render' ),
19073 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
19074 'manage_options',
19075 'account',
19076 array( &$this, '_account_page_load' ),
19077 WP_FS__DEFAULT_PRIORITY,
19078 ( $add_submenu_items && $show_account )
19079 );
19080 }
19081 }
19082
19083 if ( $add_submenu_items ) {
19084 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19085 // Add contact page.
19086 $this->add_submenu_item(
19087 $this->get_text_inline( 'Contact Us', 'contact-us' ),
19088 array( &$this, '_contact_page_render' ),
19089 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
19090 'manage_options',
19091 'contact',
19092 'Freemius::_clean_admin_content_section',
19093 WP_FS__DEFAULT_PRIORITY,
19094 $this->is_submenu_item_visible( 'contact' )
19095 );
19096 }
19097
19098 if ( $this->has_addons() ) {
19099 $this->add_submenu_item(
19100 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19101 array( &$this, '_addons_page_render' ),
19102 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19103 'manage_options',
19104 'addons',
19105 array( &$this, '_addons_page_load' ),
19106 WP_FS__LOWEST_PRIORITY - 1,
19107 $this->is_submenu_item_visible( 'addons' )
19108 );
19109 }
19110 }
19111
19112 if ( $add_submenu_items ||
19113 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
19114 ) {
19115 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19116 $show_pricing = (
19117 $this->is_submenu_item_visible( 'pricing' ) &&
19118 $this->is_pricing_page_visible()
19119 );
19120
19121 $pricing_cta_text = $this->get_pricing_cta_label();
19122 $pricing_class = 'upgrade-mode';
19123 if ( $show_pricing ) {
19124 if ( $this->is_in_trial_promotion() &&
19125 ! $this->is_paying_or_trial()
19126 ) {
19127 // If running a trial promotion, modify the pricing to load the trial.
19128 $pricing_class = 'trial-mode';
19129 } else if ( $this->is_paying() ) {
19130 $pricing_class = '';
19131 }
19132 }
19133
19134 // Add upgrade/pricing page.
19135 $this->add_submenu_item(
19136 $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' ) ),
19137 array( &$this, '_pricing_page_render' ),
19138 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19139 'manage_options',
19140 'pricing',
19141 'Freemius::_clean_admin_content_section',
19142 WP_FS__LOWEST_PRIORITY,
19143 ( $add_submenu_items && $show_pricing ),
19144 $pricing_class
19145 );
19146 }
19147 }
19148
19149 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19150 /**
19151 * Add the other menu items if there are any when not in activation mode or license activation is not
19152 * required (license activation is required for registered or anonymous users after activating the
19153 * premium version when the site is not in trial mode or there's no active valid license).
19154 *
19155 * @author Leo Fajardo (@leorw)
19156 * @since 2.2.1
19157 */
19158 if ( 0 < count( $this->_menu_items ) ) {
19159 if ( ! $this->_menu->is_top_level() ) {
19160 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19161
19162 // Append submenu items right after the plugin's submenu item.
19163 $this->order_sub_submenu_items();
19164 } else {
19165 // Append submenu items.
19166 $this->embed_submenu_items();
19167 }
19168 }
19169 }
19170 }
19171
19172 /**
19173 * Moved the actual submenu item additions to a separated function,
19174 * in order to support sub-submenu items when the plugin's settings
19175 * only have a submenu and not top-level menu item.
19176 *
19177 * @author Vova Feldman (@svovaf)
19178 * @since 1.1.4
19179 */
19180 private function embed_submenu_items() {
19181 $item_template = $this->_menu->is_top_level() ?
19182 '<span class="fs-submenu-item %s %s %s">%s</span>' :
19183 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
19184
19185 $top_level_menu_capability = $this->get_top_level_menu_capability();
19186
19187 ksort( $this->_menu_items );
19188
19189 $is_first_submenu_item = true;
19190
19191 foreach ( $this->_menu_items as $priority => $items ) {
19192 foreach ( $items as $item ) {
19193 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19194
19195 $menu_item = sprintf(
19196 $item_template,
19197 $this->get_unique_affix(),
19198 $item['menu_slug'],
19199 ! empty( $item['class'] ) ? $item['class'] : '',
19200 $item['menu_title']
19201 );
19202
19203 $top_level_menu_slug = $this->get_top_level_menu_slug();
19204 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19205
19206 if ( ! isset( $item['url'] ) ) {
19207 $hook = FS_Admin_Menu_Manager::add_subpage(
19208 $item['show_submenu'] ?
19209 $top_level_menu_slug :
19210 '',
19211 $item['page_title'],
19212 $menu_item,
19213 $capability,
19214 $menu_slug,
19215 $item['render_function']
19216 );
19217
19218 if ( false !== $item['before_render_function'] ) {
19219 add_action( "load-$hook", $item['before_render_function'] );
19220 }
19221 } else {
19222 FS_Admin_Menu_Manager::add_subpage(
19223 $item['show_submenu'] ?
19224 $top_level_menu_slug :
19225 '',
19226 $item['page_title'],
19227 $menu_item,
19228 $capability,
19229 $menu_slug,
19230 array( $this, '' )
19231 );
19232 }
19233
19234 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19235 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19236 /**
19237 * If the top-level menu has been dynamically created, remove the first submenu item that
19238 * WordPress automatically creates when there's no submenu item whose slug matches the
19239 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19240 *
19241 * Awesome Plugin
19242 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19243 *
19244 * @author Leo Fajardo (@leorw)
19245 */
19246 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19247 }
19248
19249 $is_first_submenu_item = false;
19250 }
19251 }
19252 }
19253 }
19254
19255 /**
19256 * Re-order the submenu items so all Freemius added new submenu items
19257 * are added right after the plugin's settings submenu item.
19258 *
19259 * @author Vova Feldman (@svovaf)
19260 * @since 1.1.4
19261 */
19262 private function order_sub_submenu_items() {
19263 global $submenu;
19264
19265 $menu_slug = $this->_menu->get_top_level_menu_slug();
19266
19267 /**
19268 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19269 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19270 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19271 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19272 *
19273 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19274 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19275 * capability needed to access the parent menu as the capability for the submenus that we will add.
19276 */
19277 if ( empty( $submenu[ $menu_slug ] ) ) {
19278 return;
19279 }
19280
19281 $top_level_menu = &$submenu[ $menu_slug ];
19282
19283 $all_submenu_items_after = array();
19284
19285 $found_submenu_item = false;
19286
19287 foreach ( $top_level_menu as $submenu_id => $meta ) {
19288 if ( $found_submenu_item ) {
19289 // Remove all submenu items after the plugin's submenu item.
19290 $all_submenu_items_after[] = $meta;
19291 unset( $top_level_menu[ $submenu_id ] );
19292 }
19293
19294 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19295 // Found the submenu item, put all below.
19296 $found_submenu_item = true;
19297 continue;
19298 }
19299 }
19300
19301 // Embed all plugin's new submenu items.
19302 $this->embed_submenu_items();
19303
19304 // Start with specially high number to make sure it's appended.
19305 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19306 foreach ( $all_submenu_items_after as $meta ) {
19307 $top_level_menu[ $i ] = $meta;
19308 $i ++;
19309 }
19310
19311 // Sort submenu items.
19312 ksort( $top_level_menu );
19313 }
19314
19315 /**
19316 * Helper method to return the module's support forum URL.
19317 *
19318 * @author Vova Feldman (@svovaf)
19319 * @since 1.2.2.7
19320 *
19321 * @return string
19322 */
19323 function get_support_forum_url() {
19324 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19325 }
19326
19327 /**
19328 * Displays the Support Forum link when enabled.
19329 *
19330 * Can be filtered like so:
19331 *
19332 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19333 * if ( 'support' === $menu_id ) {
19334 * return _fs->is_registered();
19335 * }
19336 * return $is_visible;
19337 * }
19338 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19339 *
19340 */
19341 function _add_default_submenu_items() {
19342 if ( ! $this->is_on() ) {
19343 return;
19344 }
19345
19346 if ( ! $this->is_activation_mode() &&
19347 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19348 ( ! $this->_is_network_active && is_admin() ) )
19349 ) {
19350 $this->add_submenu_link_item(
19351 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19352 $this->get_support_forum_url(),
19353 'wp-support-forum',
19354 null,
19355 50,
19356 $this->is_submenu_item_visible( 'support' )
19357 );
19358 }
19359 }
19360
19361 /**
19362 * @author Vova Feldman (@svovaf)
19363 * @since 1.0.1
19364 *
19365 * @param string $menu_title
19366 * @param callable $render_function
19367 * @param bool|string $page_title
19368 * @param string $capability
19369 * @param bool|string $menu_slug
19370 * @param bool|callable $before_render_function
19371 * @param int $priority
19372 * @param bool $show_submenu
19373 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19374 */
19375 function add_submenu_item(
19376 $menu_title,
19377 $render_function,
19378 $page_title = false,
19379 $capability = 'manage_options',
19380 $menu_slug = false,
19381 $before_render_function = false,
19382 $priority = WP_FS__DEFAULT_PRIORITY,
19383 $show_submenu = true,
19384 $class = ''
19385 ) {
19386 $this->_logger->entrance( 'Title = ' . $menu_title );
19387
19388 if ( $this->is_addon() ) {
19389 $parent_fs = $this->get_parent_instance();
19390
19391 if ( is_object( $parent_fs ) ) {
19392 $parent_fs->add_submenu_item(
19393 $menu_title,
19394 $render_function,
19395 $page_title,
19396 $capability,
19397 $menu_slug,
19398 $before_render_function,
19399 $priority,
19400 $show_submenu,
19401 $class
19402 );
19403
19404 return;
19405 }
19406 }
19407
19408 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19409 $this->_menu_items[ $priority ] = array();
19410 }
19411
19412 $this->_menu_items[ $priority ][] = array(
19413 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19414 'menu_title' => $menu_title,
19415 'capability' => $capability,
19416 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19417 'render_function' => $render_function,
19418 'before_render_function' => $before_render_function,
19419 'show_submenu' => $show_submenu,
19420 'class' => $class,
19421 );
19422 }
19423
19424 /**
19425 * @author Vova Feldman (@svovaf)
19426 * @since 1.0.1
19427 *
19428 * @param string $menu_title
19429 * @param string $url
19430 * @param bool $menu_slug
19431 * @param string $capability
19432 * @param int $priority
19433 * @param bool $show_submenu
19434 */
19435 function add_submenu_link_item(
19436 $menu_title,
19437 $url,
19438 $menu_slug = false,
19439 $capability = 'read',
19440 $priority = WP_FS__DEFAULT_PRIORITY,
19441 $show_submenu = true
19442 ) {
19443 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19444
19445 if ( $this->is_addon() ) {
19446 $parent_fs = $this->get_parent_instance();
19447
19448 if ( is_object( $parent_fs ) ) {
19449 $parent_fs->add_submenu_link_item(
19450 $menu_title,
19451 $url,
19452 $menu_slug,
19453 $capability,
19454 $priority,
19455 $show_submenu
19456 );
19457
19458 return;
19459 }
19460 }
19461
19462 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19463 $this->_menu_items[ $priority ] = array();
19464 }
19465
19466 $this->_menu_items[ $priority ][] = array(
19467 'menu_title' => $menu_title,
19468 'capability' => $capability,
19469 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19470 'url' => $url,
19471 'page_title' => $menu_title,
19472 'render_function' => 'fs_dummy',
19473 'before_render_function' => '',
19474 'show_submenu' => $show_submenu,
19475 );
19476 }
19477
19478 #endregion ------------------------------------------------------------------
19479
19480 #--------------------------------------------------------------------------------
19481 #region Admin Notices
19482 #--------------------------------------------------------------------------------
19483
19484 /**
19485 * @author Vova Feldman (@svovaf)
19486 * @since 2.3.1
19487 *
19488 * @param string|string[] $ids
19489 * @param int|null $network_level_or_blog_id
19490 *
19491 * @uses FS_Admin_Notices::remove_sticky()
19492 */
19493 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19494 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19495 }
19496
19497 #endregion
19498
19499 #--------------------------------------------------------------------------------
19500 #region Actions / Hooks / Filters
19501 #--------------------------------------------------------------------------------
19502
19503 /**
19504 * @author Vova Feldman (@svovaf)
19505 * @since 1.1.7
19506 *
19507 * @param string $tag
19508 *
19509 * @return string
19510 */
19511 public function get_action_tag( $tag ) {
19512 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19513 }
19514
19515 /**
19516 * @author Vova Feldman (@svovaf)
19517 * @since 1.2.1.6
19518 *
19519 * @param string $tag
19520 * @param string $slug
19521 * @param bool $is_plugin
19522 *
19523 * @return string
19524 */
19525 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19526 $action = "fs_{$tag}";
19527
19528 if ( ! empty( $slug ) ) {
19529 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19530 }
19531
19532 return $action;
19533 }
19534
19535 /**
19536 * Returns a string that can be used to generate a unique action name,
19537 * option name, HTML element ID, or HTML element class.
19538 *
19539 * @author Leo Fajardo (@leorw)
19540 * @since 1.2.2
19541 *
19542 * @return string
19543 */
19544 public function get_unique_affix() {
19545 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19546 }
19547
19548 /**
19549 * Returns a string that can be used to generate a unique action name,
19550 * option name, HTML element ID, or HTML element class.
19551 *
19552 * @author Vova Feldman (@svovaf)
19553 * @since 1.2.2.5
19554 *
19555 * @param string $slug
19556 * @param bool $is_plugin
19557 *
19558 * @return string
19559 */
19560 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19561 $affix = $slug;
19562
19563 if ( ! $is_plugin ) {
19564 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19565 }
19566
19567 return $affix;
19568 }
19569
19570 /**
19571 * @author Vova Feldman (@svovaf)
19572 * @since 1.2.1
19573 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19574 * based on the slug for backward compatibility.
19575 *
19576 * @param string $tag
19577 *
19578 * @return string
19579 */
19580 function get_ajax_action( $tag ) {
19581 return self::get_ajax_action_static( $tag, $this->_module_id );
19582 }
19583
19584 /**
19585 * @author Vova Feldman (@svovaf)
19586 * @since 1.2.1.7
19587 *
19588 * @param string $tag
19589 *
19590 * @return string
19591 */
19592 function get_ajax_security( $tag ) {
19593 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19594 }
19595
19596 /**
19597 * @author Vova Feldman (@svovaf)
19598 * @since 1.2.1.7
19599 *
19600 * @param string $tag
19601 */
19602 function check_ajax_referer( $tag ) {
19603 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19604 }
19605
19606 /**
19607 * @author Vova Feldman (@svovaf)
19608 * @since 1.2.1.6
19609 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19610 * based on the slug for backward compatibility.
19611 *
19612 * @param string $tag
19613 * @param number|null $module_id
19614 *
19615 * @return string
19616 */
19617 static function get_ajax_action_static( $tag, $module_id = null ) {
19618 $action = "fs_{$tag}";
19619
19620 if ( ! empty( $module_id ) ) {
19621 $action .= "_{$module_id}";
19622 }
19623
19624 return $action;
19625 }
19626
19627 /**
19628 * Do action, specific for the current context plugin.
19629 *
19630 * @author Vova Feldman (@svovaf)
19631 * @since 1.0.1
19632 *
19633 * @param string $tag The name of the action to be executed.
19634 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19635 * functions hooked to the action. Default empty.
19636 *
19637 * @uses do_action()
19638 */
19639 function do_action( $tag, $arg = '' ) {
19640 $args = func_get_args();
19641
19642 $this->_logger->entrance( $tag );
19643
19644 call_user_func_array( 'do_action', array_merge(
19645 array( $this->get_action_tag( $tag ) ),
19646 array_slice( $args, 1 ) )
19647 );
19648 }
19649
19650 /**
19651 * Add action, specific for the current context plugin.
19652 *
19653 * @author Vova Feldman (@svovaf)
19654 * @since 1.0.1
19655 *
19656 * @param string $tag
19657 * @param callable $function_to_add
19658 * @param int $priority
19659 * @param int $accepted_args
19660 *
19661 * @uses add_action()
19662 */
19663 function add_action(
19664 $tag,
19665 $function_to_add,
19666 $priority = WP_FS__DEFAULT_PRIORITY,
19667 $accepted_args = 1
19668 ) {
19669 $this->_logger->entrance( $tag );
19670
19671 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19672 }
19673
19674 /**
19675 * Add AJAX action, specific for the current context plugin.
19676 *
19677 * @author Vova Feldman (@svovaf)
19678 * @since 1.2.1
19679 *
19680 * @param string $tag
19681 * @param callable $function_to_add
19682 * @param int $priority
19683 *
19684 * @uses add_action()
19685 *
19686 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19687 */
19688 function add_ajax_action(
19689 $tag,
19690 $function_to_add,
19691 $priority = WP_FS__DEFAULT_PRIORITY
19692 ) {
19693 $this->_logger->entrance( $tag );
19694
19695 return self::add_ajax_action_static(
19696 $tag,
19697 $function_to_add,
19698 $priority,
19699 $this->_module_id
19700 );
19701 }
19702
19703 /**
19704 * Add AJAX action.
19705 *
19706 * @author Vova Feldman (@svovaf)
19707 * @since 1.2.1.6
19708 *
19709 * @param string $tag
19710 * @param callable $function_to_add
19711 * @param int $priority
19712 * @param number|null $module_id
19713 *
19714 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19715 * @uses add_action()
19716 *
19717 */
19718 static function add_ajax_action_static(
19719 $tag,
19720 $function_to_add,
19721 $priority = WP_FS__DEFAULT_PRIORITY,
19722 $module_id = null
19723 ) {
19724 self::$_static_logger->entrance( $tag );
19725
19726 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19727 return false;
19728 }
19729
19730 add_action(
19731 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19732 $function_to_add,
19733 $priority,
19734 0
19735 );
19736
19737 self::$_static_logger->info( "$tag AJAX callback action added." );
19738
19739 return true;
19740 }
19741
19742 /**
19743 * Send a JSON response back to an Ajax request.
19744 *
19745 * @author Vova Feldman (@svovaf)
19746 * @since 1.2.1.5
19747 *
19748 * @param mixed $response
19749 */
19750 static function shoot_ajax_response( $response ) {
19751 wp_send_json( $response );
19752 }
19753
19754 /**
19755 * Send a JSON response back to an Ajax request, indicating success.
19756 *
19757 * @author Vova Feldman (@svovaf)
19758 * @since 1.2.1.5
19759 *
19760 * @param mixed $data Data to encode as JSON, then print and exit.
19761 */
19762 static function shoot_ajax_success( $data = null ) {
19763 wp_send_json_success( $data );
19764 }
19765
19766 /**
19767 * Send a JSON response back to an Ajax request, indicating failure.
19768 *
19769 * @author Vova Feldman (@svovaf)
19770 * @since 1.2.1.5
19771 *
19772 * @param mixed $error Optional error message.
19773 */
19774 static function shoot_ajax_failure( $error = '' ) {
19775 $result = array( 'success' => false );
19776 if ( ! empty( $error ) ) {
19777 $result['error'] = $error;
19778 }
19779
19780 wp_send_json( $result );
19781 }
19782
19783 /**
19784 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19785 *
19786 * @author Vova Feldman (@svovaf)
19787 * @since 2.5.1
19788 *
19789 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19790 *
19791 * @return string
19792 */
19793 static function ajax_url( $wrap_with = "'") {
19794 if ( fs_is_network_admin() ) {
19795 $param_name = '_fs_network_admin';
19796 } else {
19797 $param_name = '_fs_blog_admin';
19798 }
19799
19800 $url = admin_url( 'admin-ajax.php', 'relative' );
19801 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19802 $url .= "{$param_name}=true";
19803
19804 return "{$wrap_with}{$url}{$wrap_with}";
19805 }
19806
19807 /**
19808 * Apply filter, specific for the current context plugin.
19809 *
19810 * @author Vova Feldman (@svovaf)
19811 * @since 1.0.9
19812 *
19813 * @param string $tag The name of the filter hook.
19814 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19815 *
19816 * @return mixed The filtered value after all hooked functions are applied to it.
19817 *
19818 * @uses apply_filters()
19819 */
19820 function apply_filters( $tag, $value ) {
19821 $args = func_get_args();
19822
19823 $this->_logger->entrance( $tag );
19824
19825 array_unshift( $args, $this->get_unique_affix() );
19826
19827 return call_user_func_array( 'fs_apply_filter', $args );
19828 }
19829
19830 /**
19831 * Add filter, specific for the current context plugin.
19832 *
19833 * @author Vova Feldman (@svovaf)
19834 * @since 1.0.9
19835 *
19836 * @param string $tag
19837 * @param callable $function_to_add
19838 * @param int $priority
19839 * @param int $accepted_args
19840 *
19841 * @uses add_filter()
19842 */
19843 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19844 $this->_logger->entrance( $tag );
19845
19846 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19847 }
19848
19849 /**
19850 * Check if has filter.
19851 *
19852 * @author Vova Feldman (@svovaf)
19853 * @since 1.1.4
19854 *
19855 * @param string $tag
19856 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19857 *
19858 * @return false|int
19859 *
19860 * @uses has_filter()
19861 */
19862 function has_filter( $tag, $function_to_check = false ) {
19863 $this->_logger->entrance( $tag );
19864
19865 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19866 }
19867
19868 #endregion
19869
19870 /**
19871 * Override default i18n text phrases.
19872 *
19873 * @author Vova Feldman (@svovaf)
19874 * @since 1.1.6
19875 *
19876 * @param string[] string $key_value
19877 *
19878 * @uses fs_override_i18n()
19879 */
19880 function override_i18n( $key_value ) {
19881 fs_override_i18n( $key_value, $this->_slug );
19882 }
19883
19884 /* Account Page
19885 ------------------------------------------------------------------------------------------------------------------*/
19886 /**
19887 * Update site information.
19888 *
19889 * @author Vova Feldman (@svovaf)
19890 * @since 1.0.1
19891 *
19892 * @param bool $store Flush to Database if true.
19893 * @param null|int $network_level_or_blog_id Since 2.0.0
19894 * @param \FS_Site $site Since 2.0.0
19895 */
19896 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
19897 $this->_logger->entrance();
19898
19899 if ( is_null( $site ) ) {
19900 $site = $this->_site;
19901 }
19902
19903 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19904 $this->_logger->error( "Empty install ID, can't store site." );
19905
19906 return;
19907 }
19908
19909 $site_clone = clone $site;
19910
19911 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19912
19913 if (
19914 ! $is_backup &&
19915 is_object( $this->_user ) && $this->_user->id != $site->user_id
19916 ) {
19917 $this->sync_user_by_current_install( $site->user_id );
19918
19919 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19920
19921 if ( empty( $prev_stored_user_id ) &&
19922 is_object($this->_user) && $this->_user->id != $site->user_id
19923 ) {
19924 /**
19925 * Store the current user ID as the previous user ID so that the previous user can be used
19926 * as the install's owner while the new owner's details are not yet available.
19927 *
19928 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19929 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19930 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19931 * into `replica`.
19932 *
19933 * @author Leo Fajardo (@leorw)
19934 */
19935 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19936 }
19937 }
19938
19939 $sites[ $this->_slug ] = $site_clone;
19940
19941 $this->set_account_option(
19942 ( $is_backup ? 'prev_' : '' ) . 'sites',
19943 $sites,
19944 $store,
19945 $network_level_or_blog_id
19946 );
19947 }
19948
19949 /**
19950 * 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).
19951 *
19952 * @author Leo Fajardo (@leorw)
19953 * @since 2.5.0
19954 */
19955 private function back_up_site() {
19956 $this->_logger->entrance();
19957
19958 $site_clone = clone $this->_site;
19959
19960 $this->_store_site( true, null, $site_clone, true );
19961 }
19962
19963 /**
19964 * Update plugin's plans information.
19965 *
19966 * @author Vova Feldman (@svovaf)
19967 * @since 1.0.2
19968 *
19969 * @param bool $store Flush to Database if true.
19970 */
19971 private function _store_plans( $store = true ) {
19972 $this->_logger->entrance();
19973
19974 $plans = self::get_all_plans( $this->_module_type );
19975
19976 // Copy plans.
19977 $encrypted_plans = array();
19978 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19979 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19980 }
19981
19982 $plans[ $this->_slug ] = $encrypted_plans;
19983
19984 $this->set_account_option( 'plans', $plans, $store );
19985 }
19986
19987 /**
19988 * Update user's plugin licenses.
19989 *
19990 * @author Vova Feldman (@svovaf)
19991 * @since 1.0.5
19992 *
19993 * @param bool $store
19994 * @param number|bool $module_id
19995 * @param FS_Plugin_License[] $licenses
19996 */
19997 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19998 $this->_logger->entrance();
19999
20000 $all_licenses = self::get_all_licenses();
20001
20002 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
20003 $module_id = $this->_module_id;
20004
20005 $user_licenses = is_array( $this->_licenses ) ?
20006 $this->_licenses :
20007 array();
20008
20009 if ( empty( $user_licenses ) ) {
20010 // If the context user doesn't have any license, don't update the licenses collection.
20011 return;
20012 }
20013
20014 $new_user_licenses_map = array();
20015 foreach ( $user_licenses as $user_license ) {
20016 $new_user_licenses_map[ $user_license->id ] = $user_license;
20017 }
20018
20019 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
20020
20021 // Update user licenses.
20022 $licenses_to_update_count = count( $new_user_licenses_map );
20023 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
20024 if ( 0 === $licenses_to_update_count ) {
20025 break;
20026 }
20027
20028 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
20029 // Update license.
20030 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
20031 unset( $new_user_licenses_map[ $license->id ] );
20032
20033 $licenses_to_update_count --;
20034 }
20035 }
20036
20037 if ( ! empty( $new_user_licenses_map ) ) {
20038 // Add new licenses.
20039 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
20040 }
20041
20042 $licenses = $all_licenses[ $module_id ];
20043 }
20044
20045 if ( ! isset( $all_licenses[ $module_id ] ) ) {
20046 $all_licenses[ $module_id ] = array();
20047 }
20048
20049 $all_licenses[ $module_id ] = $licenses;
20050
20051 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
20052 }
20053
20054 /**
20055 * Update user information.
20056 *
20057 * @author Vova Feldman (@svovaf)
20058 * @since 1.0.1
20059 *
20060 * @param bool $store Flush to Database if true.
20061 */
20062 private function _store_user( $store = true ) {
20063 $this->_logger->entrance();
20064
20065 if ( empty( $this->_user->id ) ) {
20066 $this->_logger->error( "Empty user ID, can't store user." );
20067
20068 return;
20069 }
20070
20071 $users = self::get_all_users();
20072 $users[ $this->_user->id ] = $this->_user;
20073 self::$_accounts->set_option( 'users', $users, $store );
20074 }
20075
20076 /**
20077 * Update new updates information.
20078 *
20079 * @author Vova Feldman (@svovaf)
20080 * @since 1.0.4
20081 *
20082 * @param FS_Plugin_Tag|null $update
20083 * @param bool $store Flush to Database if true.
20084 * @param bool|number $plugin_id
20085 */
20086 private function _store_update( $update, $store = true, $plugin_id = false ) {
20087 $this->_logger->entrance();
20088
20089 if ( $update instanceof FS_Plugin_Tag ) {
20090 $update->updated = time();
20091 }
20092
20093 if ( ! is_numeric( $plugin_id ) ) {
20094 $plugin_id = $this->_plugin->id;
20095 }
20096
20097 $updates = self::get_all_updates();
20098 $updates[ $plugin_id ] = $update;
20099 self::$_accounts->set_option( 'updates', $updates, $store );
20100 }
20101
20102 /**
20103 * Update new updates information.
20104 *
20105 * @author Vova Feldman (@svovaf)
20106 * @since 1.0.6
20107 *
20108 * @param FS_Plugin[] $plugin_addons
20109 * @param bool $store Flush to Database if true.
20110 */
20111 private function _store_addons( $plugin_addons, $store = true ) {
20112 $this->_logger->entrance();
20113
20114 $addons = self::get_all_addons();
20115 $addons[ $this->_plugin->id ] = $plugin_addons;
20116 self::$_accounts->set_option( 'addons', $addons, $store );
20117 }
20118
20119 /**
20120 * Delete plugin's associated add-ons.
20121 *
20122 * @author Vova Feldman (@svovaf)
20123 * @since 1.0.8
20124 *
20125 * @param bool $store
20126 *
20127 * @return bool
20128 */
20129 private function _delete_account_addons( $store = true ) {
20130 $all_addons = self::get_all_account_addons();
20131
20132 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20133 return false;
20134 }
20135
20136 unset( $all_addons[ $this->_plugin->id ] );
20137
20138 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20139
20140 return true;
20141 }
20142
20143 /**
20144 * Update account add-ons list.
20145 *
20146 * @author Vova Feldman (@svovaf)
20147 * @since 1.0.6
20148 *
20149 * @param FS_Plugin[] $addons
20150 * @param bool $store Flush to Database if true.
20151 */
20152 private function _store_account_addons( $addons, $store = true ) {
20153 $this->_logger->entrance();
20154
20155 $all_addons = self::get_all_account_addons();
20156 $all_addons[ $this->_plugin->id ] = $addons;
20157 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20158 }
20159
20160 /**
20161 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20162 * the valid user licenses will be fetched again and the account add-ons may be updated.
20163 *
20164 * @author Leo Fajardo (@leorw)
20165 * @since 2.2.4
20166 */
20167 private function purge_valid_user_licenses_cache() {
20168 if ( ! $this->is_registered() ) {
20169 return;
20170 }
20171
20172 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20173 }
20174
20175 /**
20176 * @author Leo Fajardo (@leorw)
20177 * @since 2.3.0
20178 *
20179 * @param array $all_licenses
20180 * @param number|null $site_license_id
20181 * @param bool $include_parent_licenses
20182 *
20183 * @return array
20184 */
20185 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20186 $foreign_licenses = array(
20187 'ids' => array(),
20188 'license_keys' => array()
20189 );
20190
20191 $parent_license_ids_map = array();
20192
20193 foreach ( $all_licenses as $license ) {
20194 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20195 continue;
20196 }
20197
20198 $foreign_licenses['ids'][] = $license->id;
20199 $foreign_licenses['license_keys'][] = $license->secret_key;
20200
20201 if (
20202 $include_parent_licenses &&
20203 is_object( $this->_license ) &&
20204 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20205 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20206 ) {
20207 /**
20208 * Include the parent license's info only if it has not been included before since child licenses
20209 * can have the same parent license.
20210 */
20211 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20212 $foreign_licenses['license_keys'][] = $license->secret_key;
20213
20214 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20215 }
20216 }
20217
20218 if ( empty( $foreign_licenses['ids'] ) ) {
20219 $foreign_licenses = array();
20220 }
20221
20222 return $foreign_licenses;
20223 }
20224
20225 /**
20226 * @author Leo Fajardo (@leorw)
20227 * @since 2.3.0
20228 *
20229 * @return string
20230 */
20231 private function get_valid_user_licenses_endpoint() {
20232 $user_licenses_endpoint = '/licenses.json?type=active' .
20233 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20234
20235 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20236
20237 if ( ! empty ( $foreign_licenses ) ) {
20238 $foreign_licenses = array(
20239 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20240 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20241 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20242 );
20243
20244 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20245 }
20246
20247 return $user_licenses_endpoint;
20248 }
20249
20250 /**
20251 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20252 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20253 * method filters out non–add-on product IDs and stores the add-on IDs.
20254 *
20255 * @author Leo Fajardo (@leorw)
20256 * @since 2.2.4
20257 *
20258 * @return stdClass[] array
20259 */
20260 private function fetch_valid_user_licenses() {
20261 $this->_logger->entrance();
20262
20263 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20264
20265 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20266 ! is_array( $result->licenses )
20267 ) {
20268 return array();
20269 }
20270
20271 return $result->licenses;
20272 }
20273
20274 /**
20275 * @author Leo Fajardo (@leorw)
20276 * @since 2.2.4
20277 *
20278 * @return number[] Account add-on IDs.
20279 */
20280 function get_updated_account_addons() {
20281 $addons = $this->get_addons();
20282 if ( empty( $addons ) ) {
20283 return array();
20284 }
20285
20286 $account_addons = $this->get_account_addons();
20287 if ( ! is_array( $account_addons ) ) {
20288 $account_addons = array();
20289 }
20290
20291 $user_licenses = $this->is_registered() ?
20292 $this->fetch_valid_user_licenses() :
20293 array();
20294
20295 if ( empty( $user_licenses ) ) {
20296 return $account_addons;
20297 }
20298
20299 $addon_ids = array();
20300 foreach ( $addons as $addon ) {
20301 $addon_ids[] = $addon->id;
20302 }
20303
20304 $license_product_ids = array();
20305
20306 foreach ( $user_licenses as $license ) {
20307 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20308 $license_product_ids = array_merge( $license_product_ids, $license->products );
20309 } else {
20310 $license_product_ids[] = $license->plugin_id;
20311 }
20312 }
20313
20314 // Filter out non–add-on IDs.
20315 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20316 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20317 $this->_store_account_addons( array_unique( $new_account_addons ) );
20318 }
20319
20320 return $new_account_addons;
20321 }
20322
20323 /**
20324 * Store account params in the Database.
20325 *
20326 * @author Vova Feldman (@svovaf)
20327 * @since 1.0.1
20328 *
20329 * @param null|int $blog_id Since 2.0.0
20330 */
20331 private function _store_account( $blog_id = null ) {
20332 $this->_logger->entrance();
20333
20334 $this->_store_site( false, $blog_id );
20335 $this->_store_user( false );
20336 $this->_store_plans( false );
20337 $this->_store_licenses( false );
20338
20339 self::$_accounts->store( $blog_id );
20340 }
20341
20342 /**
20343 * Sync user's information.
20344 *
20345 * @author Vova Feldman (@svovaf)
20346 * @since 1.0.3
20347 * @uses FS_Api
20348 */
20349 private function _handle_account_user_sync() {
20350 $this->_logger->entrance();
20351
20352 $api = $this->get_api_user_scope();
20353
20354 // Get user's information.
20355 $user = $api->get( '/', true );
20356
20357 if ( isset( $user->id ) ) {
20358 $this->_user->first = $user->first;
20359 $this->_user->last = $user->last;
20360 $this->_user->email = $user->email;
20361
20362 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20363
20364 if ( $user->is_verified &&
20365 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20366 ) {
20367 $this->_user->is_verified = true;
20368
20369 $this->do_action( 'account_email_verified', $user->email );
20370
20371 $this->_admin_notices->add(
20372 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20373 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20374 'success',
20375 // Make admin sticky if account menu item is invisible,
20376 // since the page will be auto redirected to the plugin's
20377 // main settings page, and the non-sticky message
20378 // will disappear.
20379 ! $is_menu_item_account_visible,
20380 'email_verified'
20381 );
20382 }
20383
20384 // Flush user details to DB.
20385 $this->_store_user();
20386
20387 $this->do_action( 'after_account_user_sync', $user );
20388
20389 /**
20390 * If account menu item is hidden, redirect to plugin's main settings page.
20391 *
20392 * @author Vova Feldman (@svovaf)
20393 * @since 1.1.6
20394 *
20395 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20396 */
20397 if ( ! $is_menu_item_account_visible ) {
20398 fs_redirect( $this->_get_admin_page_url() );
20399 }
20400 }
20401 }
20402
20403 /**
20404 * @author Vova Feldman (@svovaf)
20405 * @since 1.0.9
20406 * @uses FS_Api
20407 *
20408 * @param number|bool $license_id
20409 *
20410 * @return FS_Subscription|object|bool
20411 */
20412 private function _fetch_site_license_subscription( $license_id = false ) {
20413 $this->_logger->entrance();
20414 $api = $this->get_api_site_scope();
20415
20416 if ( ! is_numeric( $license_id ) ) {
20417 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20418 $this->_license->parent_license_id :
20419 $this->_license->id;
20420 }
20421
20422 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20423
20424 return ! isset( $result->error ) ?
20425 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20426 new FS_Subscription( $result->subscriptions[0] ) :
20427 false
20428 ) :
20429 $result;
20430 }
20431
20432 /**
20433 * @author Vova Feldman (@svovaf)
20434 * @since 1.0.4
20435 * @uses FS_Api
20436 *
20437 * @param number|bool $plan_id
20438 *
20439 * @return FS_Plugin_Plan|object
20440 */
20441 private function _fetch_site_plan( $plan_id = false ) {
20442 $this->_logger->entrance();
20443 $api = $this->get_api_site_scope();
20444
20445 if ( ! is_numeric( $plan_id ) ) {
20446 $plan_id = $this->_site->plan_id;
20447 }
20448
20449 $plan = $api->get( "/plans/{$plan_id}.json", true );
20450
20451 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20452 }
20453
20454 /**
20455 * @author Vova Feldman (@svovaf)
20456 * @since 1.0.5
20457 * @uses FS_Api
20458 *
20459 * @return FS_Plugin_Plan[]|object
20460 */
20461 private function _fetch_plugin_plans() {
20462 $this->_logger->entrance();
20463 $api = $this->get_current_or_network_user_api_scope();
20464
20465 /**
20466 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20467 */
20468 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20469
20470 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20471 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20472 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20473 }
20474
20475 $result = $result->plans;
20476 }
20477
20478 return $result;
20479 }
20480
20481 /**
20482 * @author Vova Feldman (@svovaf)
20483 * @since 2.0.0
20484 *
20485 * @param number $plan_id
20486 *
20487 * @return \FS_Plugin_Plan|object
20488 */
20489 private function fetch_plan_by_id( $plan_id ) {
20490 $this->_logger->entrance();
20491 $api = $this->get_current_or_network_user_api_scope();
20492
20493 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20494
20495 return $this->is_api_result_entity( $result ) ?
20496 new FS_Plugin_Plan( $result ) :
20497 $result;
20498 }
20499
20500 /**
20501 * @author Vova Feldman (@svovaf)
20502 * @since 1.0.5
20503 * @uses FS_Api
20504 *
20505 * @param number|bool $plugin_id
20506 * @param number|bool $site_license_id
20507 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20508 * @param number|null $blog_id
20509 *
20510 * @return FS_Plugin_License[]|object
20511 */
20512 private function _fetch_licenses(
20513 $plugin_id = false,
20514 $site_license_id = false,
20515 $foreign_licenses = array(),
20516 $blog_id = null
20517 ) {
20518 $this->_logger->entrance();
20519
20520 $api = $this->get_api_user_scope();
20521
20522 if ( ! is_numeric( $plugin_id ) ) {
20523 $plugin_id = $this->_plugin->id;
20524 }
20525
20526 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20527 if ( ! empty ( $foreign_licenses ) ) {
20528 $foreign_licenses = array(
20529 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20530 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20531 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20532 );
20533
20534 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20535 }
20536
20537 $result = $api->get( $user_licenses_endpoint, true );
20538
20539 $is_site_license_synced = false;
20540
20541 $api_errors = array();
20542
20543 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20544 is_array( $result->licenses )
20545 ) {
20546 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20547 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20548
20549 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20550 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20551 }
20552 }
20553
20554 $result = $result->licenses;
20555 } else {
20556 $api_errors[] = $result;
20557 $result = array();
20558 }
20559
20560 if ( ! $is_site_license_synced ) {
20561 if ( ! is_null( $blog_id ) ) {
20562 /**
20563 * If blog ID is not null, the request is for syncing of the license of a single site via the
20564 * network-level "Account" page.
20565 *
20566 * @author Leo Fajardo (@leorw)
20567 */
20568 $this->switch_to_blog( $blog_id );
20569 }
20570
20571 $api = $this->get_api_site_scope();
20572
20573 if ( is_numeric( $site_license_id ) ) {
20574 // Try to retrieve a foreign license that is linked to the install.
20575 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20576
20577 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20578 is_array( $api_result->licenses )
20579 ) {
20580 $licenses = $api_result->licenses;
20581
20582 if ( ! empty( $licenses ) ) {
20583 $result[] = new FS_Plugin_License( $licenses[0] );
20584 }
20585 } else {
20586 $api_errors[] = $api_result;
20587 }
20588 } else if (
20589 is_object( $this->_license ) &&
20590 /**
20591 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20592 * the FS instance that does the syncing is the parent FS instance.
20593 *
20594 * @author Leo Fajardo (@leorw)
20595 * @since 2.3.0
20596 */
20597 $this->_license->plugin_id == $plugin_id
20598 ) {
20599 $is_license_in_result = false;
20600 if ( ! empty( $result ) ) {
20601 foreach ( $result as $license ) {
20602 if ( $license->id == $this->_license->id ) {
20603 $is_license_in_result = true;
20604 break;
20605 }
20606 }
20607 }
20608
20609 if ( ! $is_license_in_result ) {
20610 // Fetch foreign license by ID and license key.
20611 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20612 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20613
20614 if ( $this->is_api_result_entity( $license ) ) {
20615 $result[] = new FS_Plugin_License( $license );
20616 } else {
20617 $api_errors[] = $license;
20618 }
20619 }
20620 }
20621
20622 if ( ! is_null( $blog_id ) ) {
20623 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20624 }
20625 }
20626
20627 if ( is_array( $result ) && 0 < count( $result ) ) {
20628 // If found at least one license, return license collection even if there are errors.
20629 return $result;
20630 }
20631
20632 if ( ! empty( $api_errors ) ) {
20633 // If found any errors and no licenses, return first error.
20634 return $api_errors[0];
20635 }
20636
20637 // Fallback to empty licenses list.
20638 return $result;
20639 }
20640
20641 /**
20642 * @author Vova Feldman (@svovaf)
20643 * @since 2.0.0
20644 *
20645 * @param number $license_id
20646 * @param string $license_key
20647 *
20648 * @return \FS_Plugin_License|object
20649 */
20650 private function fetch_license_by_key( $license_id, $license_key ) {
20651 $this->_logger->entrance();
20652
20653 $api = $this->get_current_or_network_user_api_scope();
20654
20655 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20656
20657 return $this->is_api_result_entity( $result ) ?
20658 new FS_Plugin_License( $result ) :
20659 $result;
20660 }
20661
20662 /**
20663 * @author Vova Feldman (@svovaf)
20664 * @since 1.2.0
20665 * @uses FS_Api
20666 *
20667 * @param number|bool $plugin_id
20668 * @param bool $flush
20669 *
20670 * @return FS_Payment[]|object
20671 */
20672 function _fetch_payments( $plugin_id = false, $flush = false ) {
20673 $this->_logger->entrance();
20674
20675 $api = $this->get_api_user_scope();
20676
20677 if ( ! is_numeric( $plugin_id ) ) {
20678 $plugin_id = $this->_plugin->id;
20679 }
20680
20681 $include_bundles = (
20682 is_object( $this->_plugin ) &&
20683 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20684 );
20685
20686 $result = $api->get(
20687 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20688 $flush
20689 );
20690
20691 if ( ! isset( $result->error ) ) {
20692 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20693 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20694 }
20695 $result = $result->payments;
20696 }
20697
20698 return $result;
20699 }
20700
20701 /**
20702 * @author Vova Feldman (@svovaf)
20703 * @since 1.2.1.5
20704 * @uses FS_Api
20705 *
20706 * @param bool $flush
20707 *
20708 * @return \FS_Billing|mixed
20709 */
20710 function _fetch_billing( $flush = false ) {
20711 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20712
20713 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20714
20715 if ( $this->is_api_result_entity( $billing ) ) {
20716 $billing = new FS_Billing( $billing );
20717 }
20718
20719 return $billing;
20720 }
20721
20722 /**
20723 * @author Vova Feldman (@svovaf)
20724 * @since 1.0.5
20725 *
20726 * @param FS_Plugin_License[] $licenses
20727 * @param number $module_id
20728 */
20729 private function _update_licenses( $licenses, $module_id ) {
20730 $this->_logger->entrance();
20731
20732 if ( is_array( $licenses ) ) {
20733 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20734 $licenses[ $i ]->updated = time();
20735 }
20736 }
20737
20738 $this->_store_licenses( true, $module_id, $licenses );
20739 }
20740
20741 /**
20742 * @author Vova Feldman (@svovaf)
20743 * @since 1.0.4
20744 *
20745 * @param bool|number $plugin_id
20746 * @param bool $flush Since 1.1.7.3
20747 * @param int $expiration Since 1.2.2.7
20748 * @param bool|string $newer_than Since 2.2.1
20749 *
20750 * @return object|false New plugin tag info if exist.
20751 */
20752 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20753 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20754
20755 if ( ! is_object( $latest_tag ) ) {
20756 return false;
20757 }
20758
20759 $plugin_version = $this->get_plugin_version();
20760
20761 // Check if version is actually newer.
20762 $has_new_version =
20763 // If it's an non-installed add-on then always return latest.
20764 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20765 // Compare versions.
20766 version_compare( $plugin_version, $latest_tag->version, '<' );
20767
20768 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20769
20770 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20771
20772 $this->_storage->beta_data = array(
20773 'is_beta' => $is_latest_version_beta,
20774 'version' => $latest_tag->version
20775 );
20776
20777 return $has_new_version ? $latest_tag : false;
20778 }
20779
20780 /**
20781 * @author Vova Feldman (@svovaf)
20782 * @since 1.0.5
20783 *
20784 * @param bool|number $plugin_id
20785 * @param bool $flush Since 1.1.7.3
20786 * @param int $expiration Since 1.2.2.7
20787 * @param bool|string $newer_than Since 2.2.1
20788 *
20789 * @return bool|FS_Plugin_Tag
20790 */
20791 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20792 $this->_logger->entrance();
20793
20794 if ( ! is_numeric( $plugin_id ) ) {
20795 $plugin_id = $this->_plugin->id;
20796 }
20797
20798 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20799 $updates = $this->get_all_updates();
20800
20801 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20802 }
20803
20804 /**
20805 * Check if site assigned with active license.
20806 *
20807 * @author Vova Feldman (@svovaf)
20808 * @since 1.0.6
20809 *
20810 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20811 */
20812 function has_active_license() {
20813 return (
20814 is_object( $this->_license ) &&
20815 is_numeric( $this->_license->id ) &&
20816 ! $this->_license->is_expired()
20817 );
20818 }
20819
20820 /**
20821 * Check if site assigned with active & valid (not expired) license.
20822 *
20823 * @author Vova Feldman (@svovaf)
20824 * @since 1.2.1
20825 *
20826 * @param bool $check_expiration
20827 */
20828 function has_active_valid_license( $check_expiration = true ) {
20829 return self::is_active_valid_license( $this->_license, $check_expiration );
20830 }
20831
20832 /**
20833 * @author Leo Fajardo (@leorw)
20834 * @since 2.3.1
20835 */
20836 function is_data_debug_mode() {
20837 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20838 return false;
20839 }
20840
20841 $fs = $this->is_addon() ?
20842 $this->get_parent_instance() :
20843 $this;
20844
20845 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20846 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20847 } else {
20848 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20849 }
20850
20851 return ( 'true' === $is_developer_license_debug_mode );
20852 }
20853
20854 /**
20855 * @author Leo Fajardo (@leorw)
20856 * @since 2.3.1
20857 */
20858 function _set_data_debug_mode() {
20859 if ( ! $this->is_whitelabeled( true ) ) {
20860 return;
20861 }
20862
20863 $license_or_user_key = fs_request_get( 'license_or_user_key' );
20864
20865 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20866 'true' :
20867 'false';
20868
20869 if ( 'true' === $transient_value ) {
20870 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20871 'last_license_key' :
20872 'last_license_user_key'
20873 );
20874
20875 if ( md5( $license_or_user_key ) !== $stored_key ) {
20876 $this->shoot_ajax_failure( sprintf(
20877 '%s... %s',
20878 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20879 $this->get_text_inline(
20880 'seems like the key you entered doesn\'t match our records.',
20881 'developer-or-license-not-found'
20882 )
20883 ) );
20884 }
20885 }
20886
20887 if ( $this->is_network_active() && fs_is_network_admin() ) {
20888 set_site_transient(
20889 "fs_{$this->get_id()}_data_debug_mode",
20890 $transient_value,
20891 WP_FS__TIME_24_HOURS_IN_SEC / 24
20892 );
20893 } else {
20894 set_transient(
20895 "fs_{$this->get_id()}_data_debug_mode",
20896 $transient_value,
20897 WP_FS__TIME_24_HOURS_IN_SEC / 24
20898 );
20899 }
20900
20901 if ( 'true' === $transient_value ) {
20902 $this->_admin_notices->add_sticky(
20903 $this->get_text_inline(
20904 '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.',
20905 'data_debug_mode_enabled'
20906 ),
20907 'data_debug_mode_enabled'
20908 );
20909 }
20910
20911 $this->shoot_ajax_success();
20912 }
20913
20914 /**
20915 * Check if a given license is active & valid (not expired).
20916 *
20917 * @author Vova Feldman (@svovaf)
20918 * @since 2.1.3
20919 *
20920 * @param FS_Plugin_License $license
20921 * @param bool $check_expiration
20922 *
20923 * @return bool
20924 */
20925 private static function is_active_valid_license( $license, $check_expiration = true ) {
20926 return (
20927 is_object( $license ) &&
20928 FS_Plugin_License::is_valid_id( $license->id ) &&
20929 $license->is_active() &&
20930 ( ! $check_expiration || $license->is_valid() )
20931 );
20932 }
20933
20934 /**
20935 * Checks if there's any site that is associated with an active & valid license.
20936 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20937 *
20938 * @author Vova Feldman (@svovaf)
20939 * @since 2.1.3
20940 *
20941 * @return bool
20942 */
20943 function has_any_active_valid_license() {
20944 if ( ! fs_is_network_admin() ) {
20945 return $this->has_active_valid_license();
20946 }
20947
20948 $installs = $this->get_blog_install_map();
20949 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20950
20951 foreach ( $installs as $blog_id => $install ) {
20952 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20953 continue;
20954 }
20955
20956 foreach ( $all_plugin_licenses as $license ) {
20957 if ( $license->id == $install->license_id ) {
20958 if ( self::is_active_valid_license( $license ) ) {
20959 return true;
20960 }
20961 }
20962 }
20963 }
20964
20965 return false;
20966 }
20967
20968 /**
20969 * Check if site assigned with license with enabled features.
20970 *
20971 * @author Vova Feldman (@svovaf)
20972 * @since 1.0.6
20973 *
20974 * @return bool
20975 */
20976 function has_features_enabled_license() {
20977 return (
20978 is_object( $this->_license ) &&
20979 is_numeric( $this->_license->id ) &&
20980 $this->_license->is_features_enabled()
20981 );
20982 }
20983
20984 /**
20985 * Checks if the product is activated with a bundle license.
20986 *
20987 * @author Leo Fajardo (@leorw)
20988 * @since 2.4.0
20989 *
20990 * @return bool
20991 */
20992 function is_activated_with_bundle_license() {
20993 if ( ! $this->has_features_enabled_license() ) {
20994 return false;
20995 }
20996
20997 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20998 }
20999
21000 /**
21001 * Check if user is a trial or have feature enabled license.
21002 *
21003 * @author Vova Feldman (@svovaf)
21004 * @since 1.1.7
21005 *
21006 * @return bool
21007 */
21008 function can_use_premium_code() {
21009 return $this->is_trial() || $this->has_features_enabled_license();
21010 }
21011
21012 /**
21013 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
21014 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
21015 * the context user is authenticated.
21016 *
21017 * @author Leo Fajardo (@leorw)
21018 * @since 1.2.2
21019 *
21020 * @return bool
21021 */
21022 function is_user_admin() {
21023 /**
21024 * Require a super-admin when network activated, running from the network level OR if
21025 * running from the site level but not delegated the opt-in.
21026 *
21027 * @author Vova Feldman (@svovaf)
21028 * @since 2.0.0
21029 */
21030 if ( $this->_is_network_active &&
21031 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
21032 ) {
21033 return is_super_admin();
21034 }
21035
21036 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
21037 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
21038 }
21039
21040 /**
21041 * Sync site's plan.
21042 *
21043 * @author Vova Feldman (@svovaf)
21044 * @since 1.0.3
21045 *
21046 * @uses FS_Api
21047 *
21048 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
21049 * the admin.
21050 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
21051 * network-level "Account" page.
21052 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
21053 * `_sync_plugin_license` method in order to switch to the previous blog when sending
21054 * updates for a single site in case `execute_cron` has switched to a different blog.
21055 */
21056 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
21057 $this->_logger->entrance();
21058
21059 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21060
21061 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
21062
21063 if ( $is_addon_sync ) {
21064 $this->_sync_addon_license( $plugin_id, $background );
21065 } else {
21066 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
21067 }
21068
21069 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
21070 }
21071
21072 /**
21073 * Sync plugin's add-on license.
21074 *
21075 * @author Vova Feldman (@svovaf)
21076 * @since 1.0.6
21077 * @uses FS_Api
21078 *
21079 * @param number $addon_id
21080 * @param bool $background
21081 */
21082 private function _sync_addon_license( $addon_id, $background ) {
21083 $this->_logger->entrance();
21084
21085 if ( $this->is_addon_activated( $addon_id ) ) {
21086 // If already installed, use add-on sync.
21087 $fs_addon = self::get_instance_by_id( $addon_id );
21088
21089 if (
21090 // Add-on is network activated and network integrated.
21091 $fs_addon->is_network_active() ||
21092 // Background sync cron.
21093 self::is_cron() ||
21094 // Add-on is not network activated or not network integrated.
21095 ! fs_is_network_admin()
21096 ) {
21097 $fs_addon->_sync_license( $background );
21098
21099 return;
21100 }
21101 }
21102
21103 // Validate add-on exists.
21104 $addon = $this->get_addon( $addon_id );
21105
21106 if ( ! is_object( $addon ) ) {
21107 return;
21108 }
21109
21110 // Add add-on into account add-ons.
21111 $account_addons = $this->get_account_addons();
21112 if ( ! is_array( $account_addons ) ) {
21113 $account_addons = array();
21114 }
21115 $account_addons[] = $addon->id;
21116 $account_addons = array_unique( $account_addons );
21117 $this->_store_account_addons( $account_addons );
21118
21119 // Load add-on licenses.
21120 $licenses = $this->_fetch_licenses( $addon->id );
21121
21122 // Sync add-on licenses.
21123 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21124 $this->_update_licenses( $licenses, $addon->id );
21125
21126 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21127 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21128
21129 if ( ! isset( $plans_result->error ) ) {
21130 $plans = array();
21131 foreach ( $plans_result->plans as $plan ) {
21132 $plans[] = new FS_Plugin_Plan( $plan );
21133 }
21134
21135 $this->_admin_notices->add_sticky(
21136 sprintf(
21137 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21138 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21139 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21140 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21141 $addon->title
21142 ) . ' ' . $this->get_latest_download_link(
21143 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21144 $addon_id
21145 ),
21146 'addon_plan_upgraded_' . $addon->slug,
21147 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21148 );
21149 }
21150 }
21151 }
21152 }
21153
21154 /**
21155 * Sync site's plugin plan.
21156 *
21157 * @author Vova Feldman (@svovaf)
21158 * @since 1.0.6
21159 * @uses FS_Api
21160 *
21161 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21162 * @param bool $send_installs_update Since 2.0.0
21163 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21164 * syncing its license from the network-level "Account" page (e.g.: after
21165 * activating a license only for the single install).
21166 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21167 * can be used here to switch to the previous blog in case `execute_cron`
21168 * has switched to a different blog.
21169 */
21170 private function _sync_plugin_license(
21171 $background = false,
21172 $send_installs_update = true,
21173 $is_context_single_site = false,
21174 $current_blog_id = null
21175 ) {
21176 $this->_logger->entrance();
21177
21178 $plan_change = 'none';
21179
21180 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21181
21182 if ( ! $send_installs_update ) {
21183 $site = $this->_site;
21184 } else {
21185 /**
21186 * Sync site info.
21187 *
21188 * @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.
21189 */
21190 if ( $is_site_level_sync ) {
21191 /**
21192 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21193 *
21194 * @author Leo Fajardo (@leorw)
21195 * @since 2.2.3
21196 */
21197 if ( is_numeric( $current_blog_id ) ) {
21198 $this->switch_to_blog( $current_blog_id );
21199 }
21200
21201 $result = $this->send_install_update( array(), true, true );
21202 $is_valid = $this->is_api_result_entity( $result );
21203 } else {
21204 $result = $this->send_installs_update( array(), true, true );
21205 $is_valid = $this->is_api_result_object( $result, 'installs' );
21206 }
21207
21208 if ( ! $is_valid ) {
21209 if ( $is_context_single_site ) {
21210 // Switch back to the main blog so that the following logic will have the right entities.
21211 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21212 }
21213
21214 // Show API message only if not background sync or if paying customer.
21215 if ( ! $background || $this->is_paying() ) {
21216 // Try to ping API to see if not blocked.
21217 if ( FS_Api::is_blocked( $result ) ) {
21218 /**
21219 * @author Vova Feldman (@svovaf)
21220 * @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.
21221 */
21222 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21223 // Add notice immediately if not a background sync.
21224 $add_notice = ( ! $background );
21225
21226 if ( ! $add_notice ) {
21227 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21228
21229 // We only want to add the notice after 3 consecutive failures.
21230 $add_notice = ( 3 <= $counter );
21231
21232 if ( ! $add_notice ) {
21233 /**
21234 * 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.
21235 *
21236 * 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.
21237 */
21238 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21239 }
21240 }
21241
21242 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21243 if ( $add_notice ) {
21244 self::$_global_admin_notices->add(
21245 $this->generate_api_blocked_notice_message_from_result( $result ),
21246 '',
21247 'error',
21248 $background,
21249 'api_blocked'
21250 );
21251
21252 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21253
21254 // Notice was just shown, reset connectivity counter.
21255 delete_transient( '_fs_api_connection_retry_counter' );
21256 }
21257 }
21258 } else if ( is_object( $result ) ) {
21259 // Authentication params are broken.
21260 $this->_admin_notices->add(
21261 $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 ),
21262 '',
21263 'error'
21264 );
21265 }
21266 }
21267
21268 // No reason to continue with license sync while there are API issues.
21269 return;
21270 }
21271
21272 // API is working now. Delete the transient and start afresh.
21273 delete_transient('_fs_api_connection_retry_counter');
21274
21275 if ( $is_site_level_sync ) {
21276 $site = new FS_Site( $result );
21277 } else {
21278 // Map site addresses to their blog IDs.
21279 $address_to_blog_map = $this->get_address_to_blog_map();
21280
21281 // Find the current context install.
21282 $site = null;
21283 foreach ( $result->installs as $install ) {
21284 if ( $install->id == $this->_site->id ) {
21285 $site = new FS_Site( $install );
21286 } else {
21287 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21288 $blog_id = $address_to_blog_map[ $address ];
21289
21290 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21291 }
21292 }
21293 }
21294
21295 // Sync plans.
21296 $this->_sync_plans();
21297 }
21298
21299 // Remove sticky API connectivity message.
21300 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21301
21302 if ( ! $this->has_paid_plan() ) {
21303 $this->_site = $site;
21304 $this->_store_site(
21305 true,
21306 $is_site_level_sync ?
21307 null :
21308 $this->get_network_install_blog_id()
21309 );
21310 } else {
21311 $context_blog_id = 0;
21312
21313 if ( $is_context_single_site ) {
21314 $context_blog_id = get_current_blog_id();
21315
21316 // Switch back to the main blog in order to properly sync the license.
21317 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21318 }
21319
21320 /**
21321 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21322 * associated with that ID is not included in the user's licenses collection.
21323 */
21324 $this->_sync_licenses(
21325 $site->license_id,
21326 ( $is_context_single_site ?
21327 $context_blog_id :
21328 null
21329 )
21330 );
21331
21332 if ( $is_context_single_site ) {
21333 $this->switch_to_blog( $context_blog_id );
21334 }
21335
21336 // Check if plan / license changed.
21337 if ( $site->plan_id != $this->_site->plan_id ||
21338 // Check if trial started.
21339 $site->trial_plan_id != $this->_site->trial_plan_id ||
21340 $site->trial_ends != $this->_site->trial_ends ||
21341 // Check if license changed.
21342 $site->license_id != $this->_site->license_id
21343 ) {
21344 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21345 // New trial started.
21346 $this->_site = $site;
21347 $plan_change = 'trial_started';
21348
21349 // For trial with subscription use-case.
21350 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21351
21352 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21353 $this->_site = $site;
21354 $this->_update_site_license( $new_license );
21355 $this->_store_licenses();
21356
21357 $this->_sync_site_subscription( $this->_license );
21358 }
21359 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21360 // Was in trial, but now trial expired and no license ID.
21361 // New trial started.
21362 $this->_site = $site;
21363 $plan_change = 'trial_expired';
21364 } else {
21365 $is_free = $this->is_free_plan();
21366
21367 // Make sure license exist and not expired.
21368 $new_license = is_null( $site->license_id ) ?
21369 null :
21370 $this->_get_license_by_id( $site->license_id );
21371
21372 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21373 // License cancelled.
21374 $this->_site = $site;
21375 $this->_update_site_license( $new_license );
21376 $this->_store_licenses();
21377
21378 $plan_change = 'cancelled';
21379 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21380 // The license is expired, so ignore upgrade method.
21381 $this->_site = $site;
21382 } else {
21383 // License changed.
21384 $this->_site = $site;
21385
21386 /**
21387 * IMPORTANT:
21388 * 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.
21389 *
21390 * @author Vova Feldman (@svovaf)
21391 * @since 2.0.0
21392 */
21393 $this->_update_site_license( $new_license );
21394
21395 if ( ! $is_context_single_site &&
21396 fs_is_network_admin() &&
21397 $this->_is_network_active &&
21398 $new_license->quota > 1 &&
21399 get_blog_count() > 1
21400 ) {
21401 // See if license can activated on all sites.
21402 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21403 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21404 // Open the license activation dialog box on the account page.
21405 add_action( 'admin_footer', array(
21406 &$this,
21407 '_open_license_activation_dialog_box'
21408 ) );
21409 }
21410 }
21411 }
21412
21413 $this->_store_licenses();
21414
21415 $plan_change = $is_free ?
21416 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21417 ( is_object( $new_license ) ?
21418 'changed' :
21419 'downgraded' );
21420 }
21421 }
21422
21423 // Store updated site info.
21424 $this->_store_site(
21425 true,
21426 $is_site_level_sync ?
21427 null :
21428 $this->get_network_install_blog_id()
21429 );
21430 } else {
21431 if ( ! is_object( $this->_license ) ) {
21432 $this->maybe_update_whitelabel_flag(
21433 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21434 $this->get_license_by_id( $site->license_id ) :
21435 null
21436 );
21437 } else {
21438 $this->maybe_update_whitelabel_flag( $this->_license );
21439
21440 if ( $this->_license->is_expired() ) {
21441 if ( ! $this->has_features_enabled_license() ) {
21442 $this->_deactivate_license();
21443 $plan_change = 'downgraded';
21444 } else {
21445 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21446
21447 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21448 /**
21449 * Show the expired license notice every 14 days.
21450 *
21451 * @author Leo Fajardo (@leorw)
21452 * @since 2.3.1
21453 */
21454 $plan_change = 'expired';
21455 }
21456 }
21457 }
21458 }
21459
21460 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21461 $this->_sync_site_subscription( $this->_license );
21462 }
21463 }
21464
21465 if ( ! $this->is_addon() &&
21466 $this->_site->is_beta() !== $site->is_beta()
21467 ) {
21468 // Beta flag updated.
21469 $this->_site = $site;
21470
21471 $this->_store_site(
21472 true,
21473 $is_site_level_sync ?
21474 null :
21475 $this->get_network_install_blog_id()
21476 );
21477 }
21478
21479 if ( $this->is_addon() || $this->has_addons() ) {
21480 /**
21481 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21482 * an updated valid user licenses collection will be fetched from the server which is used to also
21483 * update the account add-ons (add-ons the user has licenses for).
21484 *
21485 * @author Leo Fajardo (@leorw)
21486 * @since 2.2.4
21487 */
21488 $this->purge_valid_user_licenses_cache();
21489 }
21490 }
21491
21492 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21493
21494 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21495 switch ( $plan_change ) {
21496 case 'none':
21497 if ( ! $background && is_admin() ) {
21498 $plan = $this->is_trial() ?
21499 $this->get_trial_plan() :
21500 $this->get_plan();
21501
21502 if ( $plan->is_free() ) {
21503 $this->_admin_notices->add(
21504 sprintf(
21505 $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' ),
21506 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21507 ) . ' ' . sprintf(
21508 '<a href="%s">%s</a>',
21509 $this->contact_url(
21510 'bug',
21511 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' ),
21512 strtoupper( $plan->name )
21513 )
21514 ),
21515 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21516 ),
21517 $hmm_text
21518 );
21519 }
21520 }
21521 break;
21522 case 'upgraded':
21523 case 'activated':
21524 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21525
21526 $this->_admin_notices->remove_sticky( array(
21527 'trial_started',
21528 'trial_promotion',
21529 'trial_expired',
21530 'activation_complete',
21531 'license_expired',
21532 ) );
21533 break;
21534 case 'changed':
21535 $this->_admin_notices->add_sticky(
21536 sprintf(
21537 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21538 $this->get_plan_title()
21539 ),
21540 'plan_changed'
21541 );
21542
21543 $this->_admin_notices->remove_sticky( array(
21544 'trial_started',
21545 'trial_promotion',
21546 'trial_expired',
21547 'activation_complete',
21548 ) );
21549 break;
21550 case 'downgraded':
21551 $this->_admin_notices->add_sticky(
21552 ($this->has_free_plan() ?
21553 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 ) :
21554 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21555 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) ) ),
21556 'license_expired',
21557 $hmm_text
21558 );
21559 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21560 break;
21561 case 'cancelled':
21562 $this->_admin_notices->add(
21563 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21564 sprintf(
21565 '<a href="%s">%s</a>',
21566 $this->contact_url( 'bug' ),
21567 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21568 ),
21569 $hmm_text,
21570 'error'
21571 );
21572 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21573 break;
21574 case 'expired':
21575 $this->_admin_notices->add_sticky(
21576 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 ),
21577 'license_expired',
21578 $hmm_text
21579 );
21580
21581 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21582
21583 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21584 break;
21585 case 'trial_started':
21586 $this->add_complete_upgrade_instructions_notice(
21587 sprintf(
21588 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21589 '<i>' . $this->get_plugin_name() . '</i>'
21590 ),
21591 'trial_started',
21592 $this->get_trial_plan()->title
21593 );
21594
21595 $this->_admin_notices->remove_sticky( array(
21596 'trial_promotion',
21597 ) );
21598 break;
21599 case 'trial_expired':
21600 $this->_admin_notices->add_sticky(
21601 ($this->has_free_plan() ?
21602 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21603 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21604 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))),
21605 'trial_expired',
21606 $hmm_text
21607 );
21608 $this->_admin_notices->remove_sticky( array(
21609 'trial_started',
21610 'trial_promotion',
21611 'plan_upgraded',
21612 ) );
21613 break;
21614 }
21615 }
21616
21617 if ( 'none' !== $plan_change ) {
21618 if (
21619 ! is_object( $this->_license ) ||
21620 ! $this->_license->is_whitelabeled
21621 ) {
21622 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21623 }
21624
21625 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21626 }
21627 }
21628
21629 /**
21630 * @author Leo Fajardo (@leorw)
21631 * @since 2.5.4
21632 *
21633 * @param mixed $result
21634 *
21635 * @return string
21636 */
21637 private function generate_api_blocked_notice_message_from_result( $result ) {
21638 $api_domains = $this->apply_filters( 'api_domains', array(
21639 'api.freemius.com',
21640 'wp.freemius.com',
21641 ) );
21642
21643 $api_domains_list_items = '';
21644
21645 foreach( $api_domains as $api_domain ) {
21646 $api_domains_list_items .= "<li>{$api_domain}</li>";
21647 }
21648
21649 $error_message = sprintf(
21650 $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' ),
21651 $this->get_plugin_name(),
21652 "<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>"
21653 );
21654
21655 $error_message =
21656 "<div>{$error_message}</div>" .
21657 '<div class="fs-api-request-error-details" style="display: none">' .
21658 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21659 $result->error->message .
21660 '</div>';
21661
21662 return $error_message;
21663 }
21664
21665 /**
21666 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21667 *
21668 * @author Vova Feldman (@svovaf)
21669 * @since 2.0.0
21670 */
21671 public function _open_license_activation_dialog_box() {
21672 $vars = array( 'license_id' => $this->_site->license_id );
21673 fs_require_once_template( 'js/open-license-activation.php', $vars );
21674 }
21675
21676 /**
21677 * @author Vova Feldman (@svovaf)
21678 * @since 1.0.5
21679 *
21680 * @param bool $background
21681 * @param FS_Plugin_License|null $premium_license
21682 */
21683 protected function _activate_license( $background = false, $premium_license = null ) {
21684 $this->_logger->entrance();
21685
21686 if ( is_null( $premium_license ) ) {
21687 $license_id = fs_request_get( 'license_id' );
21688
21689 if ( is_object( $this->_site ) &&
21690 FS_Plugin_License::is_valid_id( $license_id ) &&
21691 $license_id == $this->_site->license_id
21692 ) {
21693 // License is already activated.
21694 return;
21695 }
21696
21697 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21698 $this->_get_license_by_id( $license_id ) :
21699 $this->_get_available_premium_license();
21700 }
21701
21702 if ( ! is_object( $premium_license ) ) {
21703 return;
21704 }
21705
21706 if ( ! is_object( $this->_site ) ) {
21707 // Not yet opted-in.
21708 $user = $this->get_current_or_network_user();
21709 if ( ! is_object( $user ) ) {
21710 $user = self::_get_user_by_id( $premium_license->user_id );
21711 }
21712
21713 if ( is_object( $user ) ) {
21714 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21715 } else {
21716 $this->opt_in(
21717 false,
21718 false,
21719 false,
21720 $premium_license->secret_key
21721 );
21722
21723 return;
21724 }
21725 }
21726
21727
21728 /**
21729 * If the premium license is already associated with the install, just
21730 * update the license reference (activation is not required).
21731 *
21732 * @since 1.1.9
21733 */
21734 if ( $premium_license->id == $this->_site->license_id ) {
21735 // License is already activated.
21736 $this->_update_site_license( $premium_license );
21737 $this->_store_account();
21738
21739 return;
21740 }
21741
21742 if ( $this->_site->user_id != $premium_license->user_id ) {
21743 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21744 } else {
21745 $api_request_params = array();
21746 }
21747
21748 $api = $this->get_api_site_scope();
21749 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21750
21751 if ( ! $this->is_api_result_entity( $license ) ) {
21752 if ( ! $background ) {
21753 $this->_admin_notices->add( sprintf(
21754 '%s %s',
21755 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21756 ( is_object( $license ) && isset( $license->error ) ?
21757 $license->error->message :
21758 sprintf( '%s<br><code>%s</code>',
21759 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21760 var_export( $license, true )
21761 )
21762 )
21763 ),
21764 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21765 'error'
21766 );
21767 }
21768
21769 return;
21770 }
21771
21772 $premium_license = new FS_Plugin_License( $license );
21773
21774 // Updated site plan.
21775 $site = $this->get_api_site_scope()->get( '/', true );
21776 if ( $this->is_api_result_entity( $site ) ) {
21777 $this->_site = new FS_Site( $site );
21778 }
21779 $this->_update_site_license( $premium_license );
21780
21781 $this->_store_account();
21782
21783 if ( $this->is_addon() || $this->has_addons() ) {
21784 /**
21785 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21786 * an updated valid user licenses collection will be fetched from the server which is used to also
21787 * update the account add-ons (add-ons the user has licenses for).
21788 *
21789 * @author Leo Fajardo (@leorw)
21790 * @since 2.2.4
21791 */
21792 $this->purge_valid_user_licenses_cache();
21793 }
21794
21795 if ( ! $background ) {
21796 $this->add_complete_upgrade_instructions_notice(
21797 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21798 'license_activated'
21799 );
21800 }
21801
21802 $this->_admin_notices->remove_sticky( array(
21803 'trial_promotion',
21804 'license_expired',
21805 ) );
21806 }
21807
21808 /**
21809 * @author Vova Feldman (@svovaf)
21810 * @since 1.0.5
21811 *
21812 * @param bool $show_notice
21813 */
21814 protected function _deactivate_license( $show_notice = true ) {
21815 $this->_logger->entrance();
21816
21817 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21818
21819 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21820 $this->_admin_notices->add(
21821 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() ),
21822 $hmm_text
21823 );
21824
21825 return;
21826 }
21827
21828 $api = $this->get_api_site_scope();
21829 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21830
21831 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21832 }
21833
21834 /**
21835 * @author Leo Fajardo (@leorw)
21836 * @since 2.2.1
21837 *
21838 * @param FS_Plugin_License $license
21839 * @param bool|string $hmm_text
21840 * @param bool $show_notice
21841 */
21842 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21843 if ( isset( $license->error ) ) {
21844 $this->_admin_notices->add(
21845 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21846 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21847 $hmm_text,
21848 'error'
21849 );
21850
21851 return;
21852 }
21853
21854 // Update license cache.
21855 if ( is_array( $this->_licenses ) ) {
21856 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21857 if ( $license->id == $this->_licenses[ $i ]->id ) {
21858 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21859 }
21860 }
21861 }
21862
21863 // Update site plan to default.
21864 $this->_sync_plans();
21865 $this->_site->plan_id = $this->_plans[0]->id;
21866 // Unlink license from site.
21867 $this->_update_site_license( null );
21868
21869 $this->_store_account();
21870
21871 if ( $show_notice ) {
21872 $this->_admin_notices->add(
21873 sprintf( $this->is_only_premium() ?
21874 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21875 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21876 $this->get_plan_title()
21877 ),
21878 $this->get_text_inline( 'O.K', 'ok' )
21879 );
21880 }
21881
21882 $this->_admin_notices->remove_sticky( array(
21883 'plan_upgraded',
21884 'license_activated',
21885 ) );
21886 }
21887
21888 /**
21889 * Site plan downgrade.
21890 *
21891 * @author Vova Feldman (@svovaf)
21892 * @since 1.0.4
21893 *
21894 * @return object
21895 *
21896 * @uses FS_Api
21897 */
21898 private function _downgrade_site() {
21899 $this->_logger->entrance();
21900
21901 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21902
21903 $api = $this->get_api_site_scope();
21904 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21905
21906 $plan_downgraded = false;
21907 $plan = false;
21908 if ( $this->is_api_result_entity( $site ) ) {
21909 $prev_plan_id = $this->_site->plan_id;
21910
21911 // Update new site plan id.
21912 $this->_site->plan_id = $site->plan_id;
21913
21914 $plan = $this->get_plan();
21915 $subscription = $this->_sync_site_subscription( $this->_license );
21916
21917 // Plan downgraded if plan was changed or subscription was cancelled.
21918 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21919 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21920 } else {
21921 // handle different error cases.
21922 $this->handle_license_deactivation_result(
21923 $site,
21924 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21925 );
21926 }
21927
21928 if ( ! $plan_downgraded ) {
21929 return (object) array(
21930 'error' => (object) array(
21931 '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' )
21932 )
21933 );
21934 }
21935
21936 // Remove previous sticky message about upgrade (if exist).
21937 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21938
21939 $this->_admin_notices->add(
21940 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21941 $plan->title,
21942 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21943 )
21944 );
21945
21946 // Store site updates.
21947 $this->_store_site();
21948
21949 if ( $deactivate_license &&
21950 ! FS_Plugin_License::is_valid_id( $site->license_id )
21951 ) {
21952 if ( $this->_site->is_localhost() ) {
21953 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21954 } else {
21955 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21956 }
21957
21958 // Handle successful license deactivation result.
21959 $this->handle_license_deactivation_result( $this->_license );
21960 }
21961
21962 return $site;
21963 }
21964
21965 /**
21966 * @author Vova Feldman (@svovaf)
21967 * @since 1.1.8.1
21968 *
21969 * @param bool|string $plan_name
21970 *
21971 * @return bool If trial was successfully started.
21972 */
21973 function start_trial( $plan_name = false ) {
21974 $this->_logger->entrance();
21975
21976 // Alias.
21977 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21978
21979 if ( $this->is_trial() ) {
21980 // Already in trial mode.
21981 $this->_admin_notices->add(
21982 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21983 $oops_text,
21984 'error'
21985 );
21986
21987 return false;
21988 }
21989
21990 if ( $this->_site->is_trial_utilized() ) {
21991 // Trial was already utilized.
21992 $this->_admin_notices->add(
21993 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21994 $oops_text,
21995 'error'
21996 );
21997
21998 return false;
21999 }
22000
22001 if ( false !== $plan_name ) {
22002 $plan = $this->get_plan_by_name( $plan_name );
22003
22004 if ( false === $plan ) {
22005 // Plan doesn't exist.
22006 $this->_admin_notices->add(
22007 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
22008 $oops_text,
22009 'error'
22010 );
22011
22012 return false;
22013 }
22014
22015 if ( ! $plan->has_trial() ) {
22016 // Plan doesn't exist.
22017 $this->_admin_notices->add(
22018 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
22019 $oops_text,
22020 'error'
22021 );
22022
22023 return false;
22024 }
22025 } else {
22026 if ( ! $this->has_trial_plan() ) {
22027 // None of the plans have a trial.
22028 $this->_admin_notices->add(
22029 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
22030 $oops_text,
22031 'error'
22032 );
22033
22034 return false;
22035 }
22036
22037 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
22038
22039 $plan = $plans_with_trial[0];
22040 }
22041
22042 $api = $this->get_api_site_scope();
22043 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
22044
22045 if ( ! $this->is_api_result_entity( $plan ) ) {
22046 // Some API error while trying to start the trial.
22047 $this->_admin_notices->add(
22048 $this->get_api_error_message( $plan ),
22049 $oops_text,
22050 'error'
22051 );
22052
22053 return false;
22054 }
22055
22056 // Sync license.
22057 $this->_sync_license();
22058
22059 return $this->is_trial();
22060 }
22061
22062 /**
22063 * Cancel site trial.
22064 *
22065 * @author Vova Feldman (@svovaf)
22066 * @since 1.0.9
22067 *
22068 * @return object
22069 *
22070 * @uses FS_Api
22071 */
22072 private function _cancel_trial() {
22073 $this->_logger->entrance();
22074
22075 if ( ! $this->is_trial() ) {
22076 return (object) array(
22077 'error' => (object) array(
22078 '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' )
22079 )
22080 );
22081 }
22082
22083 $trial_plan = $this->get_trial_plan();
22084
22085 $api = $this->get_api_site_scope();
22086 $site = $api->call( 'trials.json', 'delete' );
22087
22088 $trial_cancelled = false;
22089
22090 if ( $this->is_api_result_entity( $site ) ) {
22091 $prev_trial_ends = $this->_site->trial_ends;
22092
22093 if ( $this->is_paid_trial() ) {
22094 $this->_license->expiration = $site->trial_ends;
22095 $this->_license->is_cancelled = true;
22096 $this->_update_site_license( $this->_license );
22097 $this->_store_licenses();
22098
22099 // Clear subscription reference.
22100 $this->_sync_site_subscription( null );
22101 }
22102
22103 // Update site info.
22104 $this->_site = new FS_Site( $site );
22105
22106 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22107 } else {
22108 // @todo handle different error cases.
22109 }
22110
22111 if ( ! $trial_cancelled ) {
22112 return (object) array(
22113 'error' => (object) array(
22114 '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' )
22115 )
22116 );
22117 }
22118
22119 // Remove previous sticky messages about upgrade or trial (if exist).
22120 $this->_admin_notices->remove_sticky( array(
22121 'trial_started',
22122 'trial_promotion',
22123 'plan_upgraded',
22124 ) );
22125
22126 // Store site updates.
22127 $this->_store_site();
22128
22129 if ( ! $this->is_addon() ||
22130 ! $this->deactivate_premium_only_addon_without_license( true )
22131 ) {
22132 $this->_admin_notices->add(
22133 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22134 );
22135 }
22136
22137 return $site;
22138 }
22139
22140 /**
22141 * @author Vova Feldman (@svovaf)
22142 * @since 1.0.6
22143 *
22144 * @param bool|number $plugin_id
22145 *
22146 * @return bool
22147 */
22148 private function _is_addon_id( $plugin_id ) {
22149 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22150 }
22151
22152 /**
22153 * Check if user eligible to download premium version updates.
22154 *
22155 * @author Vova Feldman (@svovaf)
22156 * @since 1.0.6
22157 *
22158 * @return bool
22159 */
22160 private function _can_download_premium() {
22161 return $this->has_any_active_valid_license() ||
22162 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22163 }
22164
22165 /**
22166 *
22167 * @author Vova Feldman (@svovaf)
22168 * @since 1.0.6
22169 *
22170 * @param bool|number $addon_id
22171 * @param string $type "json" or "zip"
22172 *
22173 * @return string
22174 */
22175 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22176
22177 $is_addon = $this->_is_addon_id( $addon_id );
22178
22179 $is_premium = null;
22180 if ( ! $is_addon ) {
22181 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22182 } else if ( $this->is_addon_activated( $addon_id ) ) {
22183 $fs_addon = self::get_instance_by_id( $addon_id );
22184 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22185 }
22186
22187 // If add-on, then append add-on ID.
22188 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22189 '/updates/latest.' . $type;
22190
22191 // If add-on and not yet activated, try to fetch based on server licensing.
22192 if ( is_bool( $is_premium ) ) {
22193 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22194 }
22195
22196 if ( $this->has_secret_key() ) {
22197 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22198 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22199 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22200 }
22201
22202 return $endpoint;
22203 }
22204
22205 /**
22206 * @author Vova Feldman (@svovaf)
22207 * @since 1.0.4
22208 *
22209 * @param bool|number $addon_id
22210 * @param bool $flush Since 1.1.7.3
22211 * @param int $expiration Since 1.2.2.7
22212 * @param bool|string $newer_than Since 2.2.1
22213 * @param bool|string $fetch_readme Since 2.2.1
22214 *
22215 * @return object|false Plugin latest tag info.
22216 */
22217 function _fetch_latest_version(
22218 $addon_id = false,
22219 $flush = true,
22220 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22221 $newer_than = false,
22222 $fetch_readme = true
22223 ) {
22224 $this->_logger->entrance();
22225
22226 if ( $this->is_unresolved_clone( true ) ) {
22227 return false;
22228 }
22229
22230 $switch_to_blog_id = null;
22231
22232 /**
22233 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22234 * @since 1.1.7.4 Also check updates for add-ons.
22235 */
22236 if (
22237 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22238 ! $this->_is_addon_id( $addon_id )
22239 ) {
22240 if ( ! is_multisite() ) {
22241 return false;
22242 }
22243
22244 $installs_map = $this->get_blog_install_map();
22245
22246 foreach ( $installs_map as $blog_id => $install ) {
22247 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22248 continue;
22249 }
22250
22251 /**
22252 * @var FS_Site $install
22253 */
22254 if ( $install->is_trial() ) {
22255 $switch_to_blog_id = $blog_id;
22256 break;
22257 }
22258
22259 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22260 $license = $this->get_license_by_id( $install->license_id );
22261
22262 if ( is_object( $license ) && $license->is_features_enabled() ) {
22263 $switch_to_blog_id = $blog_id;
22264 break;
22265 }
22266 }
22267 }
22268
22269 if ( is_null( $switch_to_blog_id ) ) {
22270 return false;
22271 }
22272 }
22273
22274 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22275 get_current_blog_id() :
22276 0;
22277
22278 if ( is_numeric( $switch_to_blog_id ) ) {
22279 $this->switch_to_blog( $switch_to_blog_id );
22280 }
22281
22282 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22283
22284 if ( ! empty( $newer_than ) ) {
22285 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22286 }
22287
22288 if ( true === $fetch_readme ) {
22289 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22290 }
22291
22292 $tag = $this->get_api_site_or_plugin_scope()->get(
22293 $latest_version_endpoint,
22294 $flush,
22295 $expiration
22296 );
22297
22298 if ( is_numeric( $switch_to_blog_id ) ) {
22299 $this->switch_to_blog( $current_blog_id );
22300 }
22301
22302 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22303
22304 $this->_logger->departure( 'Latest version ' . $latest_version );
22305
22306 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22307 }
22308
22309 #----------------------------------------------------------------------------------
22310 #region Download Plugin
22311 #----------------------------------------------------------------------------------
22312
22313 /**
22314 * Download latest plugin version, based on plan.
22315 *
22316 * Not like _download_latest(), this will redirect the page
22317 * to secure download url to prevent dual download (from FS to WP server,
22318 * and then from WP server to the client / browser).
22319 *
22320 * @author Vova Feldman (@svovaf)
22321 * @since 1.0.9
22322 *
22323 * @param bool|number $plugin_id
22324 *
22325 * @uses FS_Api
22326 * @uses wp_redirect()
22327 */
22328 private function download_latest_directly( $plugin_id = false ) {
22329 $this->_logger->entrance();
22330
22331 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22332 }
22333
22334 /**
22335 * Get latest plugin FS API download URL.
22336 *
22337 * @author Vova Feldman (@svovaf)
22338 * @since 1.0.9
22339 *
22340 * @param bool|number $plugin_id
22341 *
22342 * @return string
22343 */
22344 private function get_latest_download_api_url( $plugin_id = false ) {
22345 $this->_logger->entrance();
22346
22347 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22348 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22349 );
22350
22351 return str_replace( 'http:', 'https:', $download_api_url );
22352 }
22353
22354 /**
22355 * Get payment invoice URL.
22356 *
22357 * @author Vova Feldman (@svovaf)
22358 * @since 1.2.0
22359 *
22360 * @param bool|number $payment_id
22361 *
22362 * @return string
22363 */
22364 function _get_invoice_api_url( $payment_id = false ) {
22365 $this->_logger->entrance();
22366
22367 $url = $this->get_api_user_scope()->get_signed_url(
22368 "/payments/{$payment_id}/invoice.pdf"
22369 );
22370
22371 if ( ! fs_starts_with( $url, 'https://' ) ) {
22372 // Always use HTTPS for invoices.
22373 $url = 'https' . substr( $url, 4 );
22374 }
22375
22376 return $url;
22377 }
22378
22379 /**
22380 * Get latest plugin download link.
22381 *
22382 * @author Vova Feldman (@svovaf)
22383 * @since 1.0.9
22384 *
22385 * @param string $label
22386 * @param bool|number $plugin_id
22387 *
22388 * @return string
22389 */
22390 private function get_latest_download_link( $label, $plugin_id = false ) {
22391 return sprintf(
22392 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22393 $this->_get_latest_download_local_url( $plugin_id ),
22394 $label
22395 );
22396 }
22397
22398 /**
22399 * Get latest plugin download local URL.
22400 *
22401 * @author Vova Feldman (@svovaf)
22402 * @since 1.0.9
22403 *
22404 * @param bool|number $plugin_id
22405 *
22406 * @return string
22407 */
22408 function _get_latest_download_local_url( $plugin_id = false ) {
22409 // Add timestamp to protect from caching.
22410 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22411
22412 if ( ! empty( $plugin_id ) ) {
22413 $params['plugin_id'] = $plugin_id;
22414 } else if ( $this->is_addon() ) {
22415 $params['plugin_id'] = $this->get_id();
22416 }
22417
22418 $fs = $this->is_addon() ?
22419 $this->get_parent_instance() :
22420 $this;
22421
22422 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22423 }
22424
22425 #endregion Download Plugin ------------------------------------------------------------------
22426
22427 /**
22428 * @author Vova Feldman (@svovaf)
22429 * @since 1.0.4
22430 *
22431 * @uses FS_Api
22432 *
22433 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22434 * was initiated by the admin.
22435 * @param bool|number $plugin_id
22436 * @param bool $flush Since 1.1.7.3
22437 * @param int $expiration Since 1.2.2.7
22438 * @param bool|string $newer_than Since 2.2.1
22439 */
22440 private function check_updates(
22441 $background = false,
22442 $plugin_id = false,
22443 $flush = true,
22444 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22445 $newer_than = false
22446 ) {
22447 $this->_logger->entrance();
22448
22449 // Check if there's a newer version for download.
22450 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22451
22452 $update = null;
22453 if ( is_object( $new_version ) ) {
22454 $update = new FS_Plugin_Tag( $new_version );
22455
22456 if ( ! $background ) {
22457 $this->_admin_notices->add(
22458 sprintf(
22459 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22460 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22461 $update->version,
22462 sprintf(
22463 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22464 $this->get_account_url( 'download_latest' ),
22465 sprintf(
22466 /* translators: %s: plan name (e.g. latest "Professional" version) */
22467 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22468 $this->get_plan_title()
22469 )
22470 )
22471 ),
22472 $this->get_text_inline( 'New', 'new' ) . '!'
22473 );
22474 }
22475 } else if ( false === $new_version && ! $background ) {
22476 $this->_admin_notices->add(
22477 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22478 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22479 );
22480 }
22481
22482 $this->_store_update( $update, true, $plugin_id );
22483 }
22484
22485 /**
22486 * @author Vova Feldman (@svovaf)
22487 * @since 1.0.4
22488 *
22489 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22490 *
22491 * @return FS_Plugin[]
22492 *
22493 * @uses FS_Api
22494 */
22495 private function sync_addons( $flush = false ) {
22496 $this->_logger->entrance();
22497
22498 $api = $this->get_api_site_or_plugin_scope();
22499
22500 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22501
22502 /**
22503 * @since 1.2.1
22504 *
22505 * If there's a cached version of the add-ons and not asking
22506 * for a flush, just use the currently stored add-ons.
22507 */
22508 if ( ! $flush && $api->is_cached( $path ) ) {
22509 $addons = self::get_all_addons();
22510
22511 return isset( $addons[ $this->_plugin->id ] ) ?
22512 $addons[ $this->_plugin->id ] :
22513 array();
22514 }
22515
22516 $result = $api->get( $path, $flush );
22517
22518 $addons = array();
22519 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22520 is_array( $result->plugins )
22521 ) {
22522 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22523 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22524 }
22525
22526 $this->_store_addons( $addons, true );
22527 }
22528
22529 return $addons;
22530 }
22531
22532 /**
22533 * Handle user email update.
22534 *
22535 * @author Vova Feldman (@svovaf)
22536 * @since 1.0.3
22537 * @uses FS_Api
22538 *
22539 * @param string $new_email
22540 *
22541 * @return object
22542 */
22543 private function update_email( $new_email ) {
22544 $this->_logger->entrance();
22545
22546 $api = $this->get_api_user_scope();
22547 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22548 'email' => $new_email,
22549 'after_email_confirm_url' => $this->_get_admin_page_url(
22550 'account',
22551 array( 'fs_action' => 'sync_user' )
22552 ),
22553 ) );
22554
22555 if ( ! isset( $user->error ) ) {
22556 $this->_user->email = $user->email;
22557 $this->_user->is_verified = $user->is_verified;
22558 $this->_store_user();
22559 } else {
22560 // handle different error cases.
22561 }
22562
22563 return $user;
22564 }
22565
22566 #----------------------------------------------------------------------------------
22567 #region API Error Handling
22568 #----------------------------------------------------------------------------------
22569
22570 /**
22571 * @author Vova Feldman (@svovaf)
22572 * @since 1.1.1
22573 *
22574 * @param mixed $result
22575 *
22576 * @return bool Is API result contains an error.
22577 */
22578 private function is_api_error( $result ) {
22579 return FS_Api::is_api_error( $result );
22580 }
22581
22582 /**
22583 * Checks if given API result is a non-empty and not an error object.
22584 *
22585 * @author Vova Feldman (@svovaf)
22586 * @since 1.2.1.5
22587 *
22588 * @param mixed $result
22589 * @param string|null $required_property Optional property we want to verify that is set.
22590 *
22591 * @return bool
22592 */
22593 function is_api_result_object( $result, $required_property = null ) {
22594 return FS_Api::is_api_result_object( $result, $required_property );
22595 }
22596
22597 /**
22598 * Checks if given API result is a non-empty entity object with non-empty ID.
22599 *
22600 * @author Vova Feldman (@svovaf)
22601 * @since 1.2.1.5
22602 *
22603 * @param mixed $result
22604 *
22605 * @return bool
22606 */
22607 private function is_api_result_entity( $result ) {
22608 return FS_Api::is_api_result_entity( $result );
22609 }
22610
22611 #endregion
22612
22613 /**
22614 * Make sure a given argument is an array of a specific type.
22615 *
22616 * @author Vova Feldman (@svovaf)
22617 * @since 1.2.1.5
22618 *
22619 * @param mixed $array
22620 * @param string $class
22621 *
22622 * @return bool
22623 */
22624 private function is_array_instanceof( $array, $class ) {
22625 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22626 }
22627
22628 /**
22629 * Start install ownership change.
22630 *
22631 * @author Vova Feldman (@svovaf)
22632 * @since 1.1.1
22633 * @uses FS_Api
22634 *
22635 * @param string $candidate_email
22636 * @param string $transfer_type
22637 *
22638 * @return bool Is ownership change successfully initiated.
22639 */
22640 private function init_change_owner( $candidate_email, $transfer_type ) {
22641 $this->_logger->entrance();
22642
22643 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22644 $install_ids = array();
22645
22646 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22647 $install = $install_info['install'];
22648
22649 if ( $this->_user->id != $install->user_id ) {
22650 // Skip add-on installs that are not owned by the parent product's install's owner.
22651 continue;
22652 }
22653
22654 $install_ids[ $slug ] = $install->id;
22655 }
22656
22657 $api = $this->get_api_site_scope();
22658 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22659 'email' => $candidate_email,
22660 'transfer_type' => $transfer_type,
22661 'install_ids' => implode( ',', array_values( $install_ids ) ),
22662 'after_confirm_url' => $this->_get_admin_page_url(
22663 'account',
22664 array( 'fs_action' => 'change_owner' )
22665 ),
22666 ) );
22667
22668 return ! $this->is_api_error( $result );
22669 }
22670
22671 /**
22672 * Handle install ownership change.
22673 *
22674 * @author Vova Feldman (@svovaf)
22675 * @since 1.1.1
22676 * @uses FS_Api
22677 *
22678 * @return bool Was ownership change successfully complete.
22679 */
22680 private function complete_change_owner() {
22681 $this->_logger->entrance();
22682
22683 $install_ids = fs_request_get( 'install_ids' );
22684
22685 if ( ! empty( $install_ids ) ) {
22686 $install_ids = explode( ',', $install_ids );
22687
22688 foreach ( $install_ids as $key => $install_id ) {
22689 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22690 unset( $install_ids[ $key ] );
22691 }
22692 }
22693 }
22694
22695 if ( ! is_array( $install_ids ) ) {
22696 $install_ids = array();
22697 }
22698
22699 $user = new FS_User();
22700 $user->id = fs_request_get( 'user_id' );
22701 $user->public_key = fs_request_get( 'user_public_key' );
22702 $user->secret_key = fs_request_get( 'user_secret_key' );
22703
22704 $prev_user = $this->_user;
22705 $this->_user = $user;
22706
22707 $result = $this->get_api_user_scope( true )->get(
22708 "/installs.json?install_ids=" . implode( ',', $install_ids )
22709 );
22710
22711 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22712
22713 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22714 $site_id_slug_map = array();
22715
22716 foreach ( $current_blog_sites as $slug => $site ) {
22717 $site_id_slug_map[ $site->id ] = $slug;
22718 }
22719
22720 foreach ( $result->installs as $install ) {
22721 $site = new FS_Site( $install );
22722
22723 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22724 continue;
22725 }
22726
22727 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22728
22729 if ( $this->_site->id == $site->id ) {
22730 $this->_site = $site;
22731 }
22732 }
22733 }
22734
22735 // Validate install's user and given user.
22736 if ( $user->id != $this->_site->user_id ) {
22737 $this->_user = $prev_user;
22738
22739 return false;
22740 }
22741
22742 $this->set_account_option( 'sites', $current_blog_sites, true );
22743
22744 // Fetch new user information.
22745 $user_result = $this->get_api_user_scope( true )->get();
22746 $user = new FS_User( $user_result );
22747 $this->_user = $user;
22748
22749 $this->_set_account( $user, $this->_site );
22750
22751 $remove_user = true;
22752 $all_modules_sites = self::get_all_modules_sites();
22753
22754 foreach ( $all_modules_sites as $sites_by_module_type ) {
22755 foreach ( $sites_by_module_type as $sites_by_slug ) {
22756 foreach ( $sites_by_slug as $site ) {
22757 if ( $prev_user->id == $site->user_id ) {
22758 $remove_user = false;
22759 break;
22760 }
22761 }
22762
22763 if ( ! $remove_user ) {
22764 break;
22765 }
22766 }
22767
22768 if ( ! $remove_user ) {
22769 break;
22770 }
22771 }
22772
22773 if ( $remove_user ) {
22774 $users = self::get_all_users();
22775
22776 if ( isset( $users[ $prev_user->id ] ) ) {
22777 unset( $users[ $prev_user->id ] );
22778 } else {
22779 // If the prev user wasn't found by the key, iterate over the users collection.
22780 foreach ( $users as $key => $user ) {
22781 if ( $user->id == $prev_user->id ) {
22782 unset( $users[ $key ] );
22783 break;
22784 }
22785 }
22786 }
22787
22788 $this->set_account_option( 'users', $users, true );
22789 }
22790
22791 return true;
22792 }
22793
22794 /**
22795 * Completes ownership change by license.
22796 *
22797 * @author Leo Fajardo (@leorw)
22798 * @since 2.3.2
22799 *
22800 * @param number $user_id
22801 * @param array[string]number $install_ids_by_slug_map
22802 *
22803 */
22804 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22805 $this->_logger->entrance();
22806
22807 $this->sync_user_by_current_install( $user_id );
22808
22809 $result = $this->get_api_user_scope( true )->get(
22810 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22811 );
22812
22813 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22814 $sites = self::get_all_sites( $this->get_module_type() );
22815 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22816
22817 foreach ( $result->installs as $install ) {
22818 $site = new FS_Site( $install );
22819
22820 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22821 }
22822
22823 $this->set_account_option( 'sites', $sites, true );
22824 }
22825 }
22826
22827 /**
22828 * Handle user name update.
22829 *
22830 * @author Vova Feldman (@svovaf)
22831 * @since 1.0.9
22832 * @uses FS_Api
22833 *
22834 * @return object
22835 */
22836 private function update_user_name() {
22837 $this->_logger->entrance();
22838 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22839
22840 $api = $this->get_api_user_scope();
22841 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22842 'name' => $name,
22843 ) );
22844
22845 if ( ! isset( $user->error ) ) {
22846 $this->_user->first = $user->first;
22847 $this->_user->last = $user->last;
22848 $this->_store_user();
22849 } else {
22850 // handle different error cases.
22851
22852 }
22853
22854 return $user;
22855 }
22856
22857 /**
22858 * Verify user email.
22859 *
22860 * @author Vova Feldman (@svovaf)
22861 * @since 1.0.3
22862 * @uses FS_Api
22863 */
22864 private function verify_email() {
22865 $this->_handle_account_user_sync();
22866
22867 if ( $this->_user->is_verified() ) {
22868 return;
22869 }
22870
22871 $api = $this->get_api_site_scope();
22872 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22873 'after_email_confirm_url' => $this->_get_admin_page_url(
22874 'account',
22875 array( 'fs_action' => 'sync_user' )
22876 )
22877 ) );
22878
22879 if ( ! isset( $result->error ) ) {
22880 $this->_admin_notices->add( sprintf(
22881 $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' ),
22882 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22883 ) );
22884 } else {
22885 // handle different error cases.
22886
22887 }
22888 }
22889
22890 /**
22891 * @author Vova Feldman (@svovaf)
22892 * @since 1.1.2
22893 *
22894 * @param array $params
22895 * @param bool|null $network
22896 *
22897 * @return string
22898 */
22899 function get_activation_url( $params = array(), $network = null ) {
22900 if ( $this->is_addon() && $this->has_free_plan() ) {
22901 /**
22902 * @author Vova Feldman (@svovaf)
22903 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22904 */
22905 return $this->get_parent_instance()->get_activation_url( $params );
22906 }
22907
22908 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22909 }
22910
22911 /**
22912 * @author Vova Feldman (@svovaf)
22913 * @since 1.2.1.5
22914 *
22915 * @param array $params
22916 *
22917 * @return string
22918 */
22919 function get_reconnect_url( $params = array() ) {
22920 $params['fs_action'] = 'reset_anonymous_mode';
22921 $params['fs_unique_affix'] = $this->get_unique_affix();
22922
22923 return $this->get_activation_url( $params );
22924 }
22925
22926 /**
22927 * Get the URL of the page that should be loaded after the user connect
22928 * or skip in the opt-in screen.
22929 *
22930 * @author Vova Feldman (@svovaf)
22931 * @since 1.1.3
22932 *
22933 * @param string $filter Filter name.
22934 * @param array $params Since 1.2.2.7
22935 * @param bool|null $network
22936 *
22937 * @return string
22938 */
22939 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22940 if ( $this->show_opt_in_on_themes_page() &&
22941 ( fs_request_has( 'pending_activation' ) ||
22942 // For cases when the first time path is set, even though it's a WP.org theme.
22943 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22944 ) {
22945 $first_time_path = '';
22946 } else {
22947 $first_time_path = $this->_menu->get_first_time_path(
22948 fs_is_network_admin() && $this->_is_network_active
22949 );
22950 }
22951
22952 if ( $this->_is_network_active &&
22953 fs_is_network_admin() &&
22954 ! $this->_menu->has_network_menu() &&
22955 $this->is_network_registered()
22956 ) {
22957 $target_url = $this->get_account_url();
22958 } else {
22959 // Default plugin's page.
22960 $target_url = $this->_get_admin_page_url( '', array(), $network );
22961 }
22962
22963 return add_query_arg( $params, $this->apply_filters(
22964 $filter,
22965 empty( $first_time_path ) ?
22966 $target_url :
22967 $first_time_path
22968 ) );
22969 }
22970
22971 /**
22972 * Handle account page updates / edits / actions.
22973 *
22974 * @author Vova Feldman (@svovaf)
22975 * @since 1.0.2
22976 *
22977 */
22978 private function _handle_account_edits() {
22979 if ( ! $this->is_user_admin() ) {
22980 return;
22981 }
22982
22983 $action = fs_get_action();
22984
22985 if ( empty( $action ) ) {
22986 return;
22987 }
22988
22989 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22990 $install_id = fs_request_get( 'install_id', '' );
22991
22992 // Alias.
22993 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22994
22995 $is_network_action = $this->is_network_level_action();
22996 $blog_id = $this->is_network_level_site_specific_action();
22997 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22998
22999 if ( is_numeric( $blog_id ) ) {
23000 $this->switch_to_blog( $blog_id );
23001 } else {
23002 $blog_id = '';
23003 }
23004
23005 switch ( $action ) {
23006 case 'opt_in':
23007 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23008
23009 if ( $is_parent_plugin_action ) {
23010 if ( $is_network_action && ! empty( $blog_id ) ) {
23011 if ( ! $this->is_registered() ) {
23012 $this->install_with_user(
23013 $this->get_network_user(),
23014 false,
23015 false,
23016 false,
23017 false
23018 );
23019
23020 $this->_admin_notices->add(
23021 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
23022 $this->get_text_inline( 'Awesome', 'awesome' )
23023 );
23024 }
23025 }
23026 }
23027 break;
23028
23029 case 'toggle_tracking':
23030 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23031
23032 if ( $is_parent_plugin_action ) {
23033 if ( $is_network_action && ! empty( $blog_id ) ) {
23034 if ( $this->is_registered( true ) ) {
23035 if ( $this->is_tracking_prohibited( $blog_id ) ) {
23036 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
23037 $this->_admin_notices->add(
23038 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>" ),
23039 $this->get_text_inline( 'Thank you!', 'thank-you' )
23040 );
23041 }
23042 } else {
23043 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
23044 $install = $this->get_install_by_blog_id( $blog_id );
23045
23046 $this->_admin_notices->add(
23047 sprintf(
23048 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
23049 self::get_unfiltered_site_url( $blog_id, true ),
23050 "<b>{$this->get_plugin_title()}</b>"
23051 )
23052 );
23053 }
23054 }
23055 }
23056 }
23057 }
23058
23059 break;
23060
23061 case 'delete_account':
23062 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23063
23064 $is_network_deletion = $is_network_action && empty( $blog_id );
23065
23066 if ( $is_parent_plugin_action ) {
23067 // Delete add-on installs if have any.
23068 $installed_addons = $this->get_installed_addons();
23069 foreach ( $installed_addons as $fs_addon ) {
23070 if ( $is_network_deletion ) {
23071 $fs_addon->delete_network_account_event();
23072 } else {
23073 $fs_addon->delete_account_event();
23074 }
23075 }
23076
23077 if ( $is_network_deletion ) {
23078 $this->delete_network_account_event();
23079 } else {
23080 $this->delete_account_event();
23081 }
23082
23083 // Clear user and site.
23084 $this->_site = null;
23085 $this->_user = null;
23086
23087 $this->maybe_set_slug_and_network_menu_exists_flag();
23088
23089 fs_redirect( $this->get_activation_url() );
23090 } else {
23091 if ( $this->is_addon_activated( $plugin_id ) ) {
23092 $fs_addon = self::get_instance_by_id( $plugin_id );
23093
23094 if ( $is_network_deletion ) {
23095 $fs_addon->delete_network_account_event();
23096 } else {
23097 $fs_addon->delete_account_event();
23098 }
23099
23100 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23101 }
23102 }
23103
23104 return;
23105
23106 case 'downgrade_account':
23107 if ( is_numeric( $blog_id ) ) {
23108 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23109 } else {
23110 check_admin_referer( $action );
23111 }
23112
23113 $switch_to_network_install_blog_after_cancellation = (
23114 is_numeric( $blog_id ) &&
23115 $plugin_id == $this->get_id() &&
23116 ! $this->is_trial()
23117 );
23118
23119 $result = $this->cancel_subscription_or_trial( $plugin_id );
23120 if ( $this->is_api_error( $result ) ) {
23121 $this->_admin_notices->add(
23122 $result->error->message,
23123 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23124 'error'
23125 );
23126 }
23127
23128 if ( $switch_to_network_install_blog_after_cancellation ) {
23129 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23130 }
23131
23132 return;
23133
23134 case 'activate_license':
23135 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23136
23137 $fs = $this;
23138 if ( $plugin_id != $this->get_id() ) {
23139 $fs = $this->is_addon_activated( $plugin_id ) ?
23140 self::get_instance_by_id( $plugin_id ) :
23141 null;
23142 }
23143
23144 if ( is_object( $fs ) ) {
23145 $fs->_activate_license();
23146
23147 /**
23148 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23149 *
23150 * @author Leo Fajardo (@leorw)
23151 * @since 2.4.0
23152 */
23153 unset( $_REQUEST['plugin_id'] );
23154
23155 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23156 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23157 }
23158 }
23159
23160 return;
23161
23162 case 'deactivate_license':
23163 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23164
23165 if ( $plugin_id == $this->get_id() ) {
23166 $this->_deactivate_license();
23167
23168 if ( $this->is_only_premium() ) {
23169 // Clear user and site.
23170 $this->_site = null;
23171 $this->_user = null;
23172
23173 if ( ! $is_network_action ) {
23174 fs_redirect( $this->get_activation_url() );
23175 } else if ( is_numeric( $blog_id ) ) {
23176 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23177 }
23178 }
23179 } else {
23180 if ( $this->is_addon_activated( $plugin_id ) ) {
23181 $fs_addon = self::get_instance_by_id( $plugin_id );
23182 $fs_addon->_deactivate_license();
23183 }
23184 }
23185
23186 return;
23187
23188 case 'check_updates':
23189 check_admin_referer( $action );
23190 $this->check_updates();
23191
23192 return;
23193
23194 case 'change_owner':
23195 $state = fs_request_get( 'state', 'init' );
23196 switch ( $state ) {
23197 case 'init':
23198 $candidate_email = fs_request_get( 'candidate_email' );
23199 $transfer_type = fs_request_get( 'transfer_type' );
23200
23201 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23202 if ( 'transfer' === $transfer_type ) {
23203 $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>' ) );
23204 } else {
23205 $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>' ) );
23206 }
23207 }
23208 break;
23209 case 'owner_confirmed':
23210 $candidate_email = fs_request_get( 'candidate_email', '' );
23211
23212 $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>' ) );
23213 break;
23214 case 'candidate_confirmed':
23215 if ( $this->complete_change_owner() ) {
23216 $this->_admin_notices->add_sticky(
23217 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23218 'ownership_changed',
23219 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23220 );
23221 } else {
23222 // @todo Handle failed ownership change message.
23223 }
23224 break;
23225 }
23226
23227 return;
23228
23229 case 'update_user_name':
23230 check_admin_referer( 'update_user_name' );
23231
23232 $result = $this->update_user_name();
23233
23234 if ( isset( $result->error ) ) {
23235 $this->_admin_notices->add(
23236 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23237 $oops_text,
23238 'error'
23239 );
23240 } else {
23241 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23242 }
23243
23244 return;
23245
23246 #region Actions that might be called from external links (e.g. email)
23247
23248 case 'cancel_trial':
23249 $result = $this->cancel_subscription_or_trial( $plugin_id );
23250 if ( $this->is_api_error( $result ) ) {
23251 $this->_admin_notices->add(
23252 $result->error->message,
23253 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23254 'error'
23255 );
23256 }
23257
23258 return;
23259
23260 case 'verify_email':
23261 $this->verify_email();
23262
23263 return;
23264
23265 case 'sync_user':
23266 $this->_handle_account_user_sync();
23267
23268 return;
23269
23270 case $this->get_unique_affix() . '_sync_license':
23271 $this->_sync_license();
23272
23273 return;
23274
23275 case 'download_latest':
23276 $this->download_latest_directly( $plugin_id );
23277
23278 return;
23279
23280 #endregion
23281 }
23282
23283 if ( WP_FS__IS_POST_REQUEST ) {
23284 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23285 foreach ( $properties as $p ) {
23286 if ( 'update_' . $p === $action ) {
23287 check_admin_referer( $action );
23288
23289 $this->_logger->log( $action );
23290
23291 $site_property = substr( $p, strlen( 'site_' ) );
23292 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23293 $this->get_site()->{$site_property} = $site_property_value;
23294
23295 // Store account after modification.
23296 $this->_store_site();
23297
23298 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23299
23300 $this->_admin_notices->add( sprintf(
23301 /* translators: %s: User's account property (e.g. email address, name) */
23302 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23303 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23304 ) );
23305
23306 return;
23307 }
23308 }
23309 }
23310 }
23311
23312 /**
23313 * Account page resources load.
23314 *
23315 * @author Vova Feldman (@svovaf)
23316 * @since 1.0.6
23317 */
23318 function _account_page_load() {
23319 $this->_logger->entrance();
23320
23321 $this->_logger->info( var_export( $_REQUEST, true ) );
23322
23323 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23324
23325 if ( $this->has_addons() ) {
23326 wp_enqueue_script( 'plugin-install' );
23327 add_thickbox();
23328
23329 function fs_addons_body_class( $classes ) {
23330 $classes .= ' plugins-php';
23331
23332 return $classes;
23333 }
23334
23335 add_filter( 'admin_body_class', 'fs_addons_body_class' );
23336 }
23337
23338 if ( $this->has_paid_plan() &&
23339 ! $this->has_any_license() &&
23340 ! $this->is_sync_executed() &&
23341 $this->is_tracking_allowed()
23342 ) {
23343 /**
23344 * If no licenses found and no sync job was executed during the last 24 hours,
23345 * just execute the sync job right away (blocking execution).
23346 *
23347 * @since 1.1.7.3
23348 */
23349 $this->run_manual_sync();
23350 }
23351
23352 $this->_handle_account_edits();
23353
23354 if (
23355 is_object( $this->_license ) &&
23356 $this->_license->user_id == $this->_user->id &&
23357 ! $this->is_whitelabeled( true )
23358 ) {
23359 $this->_admin_notices->add(
23360 sprintf(
23361 $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' ),
23362 sprintf(
23363 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23364 $this->get_text_inline( 'Click here', 'click-here' )
23365 )
23366 ),
23367 '',
23368 'success',
23369 false,
23370 'license_not_whitelabeled'
23371 );
23372 }
23373
23374 $this->do_action( 'account_page_load_before_departure' );
23375 }
23376
23377 /**
23378 * Renders the "Affiliation" page.
23379 *
23380 * @author Leo Fajardo (@leorw)
23381 * @since 1.2.3
23382 */
23383 function _affiliation_page_render() {
23384 $this->_logger->entrance();
23385
23386 $this->fetch_affiliate_and_terms();
23387
23388 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23389
23390 $is_bundle_context = $this->has_bundle_context();
23391
23392 $plugin_title = $this->get_plugin_title();
23393
23394 if ( $is_bundle_context ) {
23395 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23396
23397 // Add the suffix "Bundle" only if the word is not present in the title itself.
23398 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23399 $plugin_title = $this->apply_filters(
23400 'formatted_bundle_title',
23401 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23402 );
23403 }
23404 }
23405
23406 $vars = array(
23407 'id' => $this->_module_id,
23408 'plugin_title' => $plugin_title,
23409 );
23410 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23411 }
23412
23413
23414 /**
23415 * Render account page.
23416 *
23417 * @author Vova Feldman (@svovaf)
23418 * @since 1.0.0
23419 */
23420 function _account_page_render() {
23421 $this->_logger->entrance();
23422
23423 $template = 'account.php';
23424 $vars = array( 'id' => $this->_module_id );
23425
23426 /**
23427 * Added filter to the template to allow developers wrapping the template
23428 * in custom HTML (e.g. within a wizard/tabs).
23429 *
23430 * @author Vova Feldman (@svovaf)
23431 * @since 1.2.1.6
23432 */
23433 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23434 }
23435
23436 /**
23437 * Render account connect page.
23438 *
23439 * @author Vova Feldman (@svovaf)
23440 * @since 1.0.7
23441 */
23442 function _connect_page_render() {
23443 $this->_logger->entrance();
23444
23445 $vars = array( 'id' => $this->_module_id );
23446
23447 /**
23448 * Added filter to the template to allow developers wrapping the template
23449 * in custom HTML (e.g. within a wizard/tabs).
23450 *
23451 * @author Vova Feldman (@svovaf)
23452 * @since 1.2.1.6
23453 */
23454 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23455 }
23456
23457 /**
23458 * Load required resources before add-ons page render.
23459 *
23460 * @author Vova Feldman (@svovaf)
23461 * @since 1.0.6
23462 */
23463 function _addons_page_load() {
23464 $this->_logger->entrance();
23465
23466 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23467
23468 wp_enqueue_script( 'plugin-install' );
23469 add_thickbox();
23470
23471 function fs_addons_body_class( $classes ) {
23472 $classes .= ' plugins-php';
23473
23474 return $classes;
23475 }
23476
23477 add_filter( 'admin_body_class', 'fs_addons_body_class' );
23478
23479 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23480 $this->_admin_notices->add(
23481 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>' ),
23482 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23483 'update-nag'
23484 );
23485 }
23486 }
23487
23488 /**
23489 * Render add-ons page.
23490 *
23491 * @author Vova Feldman (@svovaf)
23492 * @since 1.0.6
23493 */
23494 function _addons_page_render() {
23495 $this->_logger->entrance();
23496
23497 $vars = array( 'id' => $this->_module_id );
23498
23499 /**
23500 * Added filter to the template to allow developers wrapping the template
23501 * in custom HTML (e.g. within a wizard/tabs).
23502 *
23503 * @author Vova Feldman (@svovaf)
23504 * @since 1.2.1.6
23505 */
23506 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23507 }
23508
23509 /* Pricing & Upgrade
23510 ------------------------------------------------------------------------------------------------------------------*/
23511 /**
23512 * Render pricing page.
23513 *
23514 * @author Vova Feldman (@svovaf)
23515 * @since 1.0.0
23516 */
23517 function _pricing_page_render() {
23518 $this->_logger->entrance();
23519
23520 $vars = array( 'id' => $this->_module_id );
23521
23522 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23523 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23524 } else {
23525 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23526 }
23527 }
23528
23529 /**
23530 * @author Leo Fajardo (@leorw)
23531 * @since 2.3.1
23532 */
23533 function _maybe_add_pricing_ajax_handler() {
23534 if ( ! $this->should_use_external_pricing() ) {
23535 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23536 }
23537 }
23538
23539 /**
23540 * @author Leo Fajardo (@leorw)
23541 * @since 2.3.1
23542 */
23543 function _fs_pricing_ajax_action_handler() {
23544 $this->check_ajax_referer( 'pricing_ajax_action' );
23545
23546 $result = null;
23547 $pricing_action = fs_request_get( 'pricing_action' );
23548
23549 switch ( $pricing_action ) {
23550 case 'fetch_pricing_data':
23551 $params = array(
23552 'is_enriched' => true,
23553 'trial' => fs_request_get_bool( 'trial' ),
23554 'sandbox' => fs_request_get( 'sandbox' ),
23555 's_ctx_type' => fs_request_get( 's_ctx_type' ),
23556 's_ctx_id' => fs_request_get( 's_ctx_id' ),
23557 's_ctx_ts' => fs_request_get( 's_ctx_ts' ),
23558 's_ctx_secure' => fs_request_get( 's_ctx_secure' ),
23559 );
23560
23561 $bundle_id = $this->get_bundle_id();
23562 $bundle_public_key = $this->get_bundle_public_key();
23563
23564 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23565
23566 if ( ! $has_bundle_context ) {
23567 $api = $this->get_api_plugin_scope();
23568 } else {
23569 $api = FS_Api::instance(
23570 $bundle_id,
23571 'plugin',
23572 $bundle_id,
23573 $bundle_public_key,
23574 ! $this->is_live(),
23575 false,
23576 $this->get_sdk_version()
23577 );
23578
23579 $params['plugin_id'] = $this->get_id();
23580 $params['plugin_public_key'] = $this->get_public_key();
23581 }
23582
23583 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23584 break;
23585 case 'start_trial':
23586 $result = $this->opt_in(
23587 false,
23588 false,
23589 false,
23590 false,
23591 false,
23592 fs_request_get( 'plan_id' )
23593 );
23594 }
23595
23596 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23597 $this->_logger->api_error( $result );
23598
23599 self::shoot_ajax_failure(
23600 isset( $result->error ) ?
23601 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23602 var_export( $result, true )
23603 );
23604 }
23605
23606 $this->shoot_ajax_success( $result );
23607 }
23608
23609 #----------------------------------------------------------------------------------
23610 #region Contact Us
23611 #----------------------------------------------------------------------------------
23612
23613 /**
23614 * Render contact-us page.
23615 *
23616 * @author Vova Feldman (@svovaf)
23617 * @since 1.0.3
23618 */
23619 function _contact_page_render() {
23620 $this->_logger->entrance();
23621
23622 $vars = array( 'id' => $this->_module_id );
23623
23624 /**
23625 * Added filter to the template to allow developers wrapping the template
23626 * in custom HTML (e.g. within a wizard/tabs).
23627 *
23628 * @author Vova Feldman (@svovaf)
23629 * @since 2.1.3
23630 */
23631 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23632 }
23633
23634 #endregion ------------------------------------------------------------------------
23635
23636 /**
23637 * Hide all admin notices to prevent distractions.
23638 *
23639 * @author Vova Feldman (@svovaf)
23640 * @since 1.0.3
23641 *
23642 * @uses remove_all_actions()
23643 */
23644 private static function _hide_admin_notices() {
23645 remove_all_actions( 'admin_notices' );
23646 remove_all_actions( 'network_admin_notices' );
23647 remove_all_actions( 'all_admin_notices' );
23648 remove_all_actions( 'user_admin_notices' );
23649 }
23650
23651 static function _clean_admin_content_section_hook() {
23652 $hide_admin_notices = true;
23653
23654 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23655 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23656
23657 $hide_admin_notices = false;
23658 }
23659
23660 if ( $hide_admin_notices ) {
23661 self::_hide_admin_notices();
23662 }
23663
23664 // Hide footer.
23665 echo '<style>#wpfooter { display: none !important; }</style>';
23666 }
23667
23668 /**
23669 * Attach to admin_head hook to hide all admin notices.
23670 *
23671 * @author Vova Feldman (@svovaf)
23672 * @since 1.0.3
23673 */
23674 static function _clean_admin_content_section() {
23675 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23676 }
23677
23678 /* CSS & JavaScript
23679 ------------------------------------------------------------------------------------------------------------------*/
23680 /* function _enqueue_script($handle, $src) {
23681 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23682
23683 $this->_logger->entrance( 'script = ' . $url );
23684
23685 wp_enqueue_script( $handle, $url );
23686 }*/
23687
23688 /* SDK
23689 ------------------------------------------------------------------------------------------------------------------*/
23690 private $_user_api;
23691
23692 /**
23693 *
23694 * @author Vova Feldman (@svovaf)
23695 * @since 1.0.2
23696 *
23697 * @param bool $flush
23698 *
23699 * @return FS_Api
23700 */
23701 function get_api_user_scope( $flush = false ) {
23702 if ( ! isset( $this->_user_api ) || $flush ) {
23703 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23704 }
23705
23706 return $this->_user_api;
23707 }
23708
23709 /**
23710 * @author Vova Feldman (@svovaf)
23711 * @since 2.0.0
23712 *
23713 * @param \FS_User $user
23714 *
23715 * @return \FS_Api
23716 */
23717 private function get_api_user_scope_by_user( FS_User $user ) {
23718 return FS_Api::instance(
23719 $this->_module_id,
23720 'user',
23721 $user->id,
23722 $user->public_key,
23723 ! $this->is_live(),
23724 $user->secret_key,
23725 $this->get_sdk_version()
23726 );
23727 }
23728
23729 /**
23730 *
23731 * @author Leo Fajardo (@leorw)
23732 * @since 2.0.0
23733 *
23734 * @param bool $flush
23735 *
23736 * @return FS_Api
23737 */
23738 private function get_current_or_network_user_api_scope( $flush = false ) {
23739 if ( ! $this->_is_network_active ||
23740 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23741 ) {
23742 return $this->get_api_user_scope( $flush );
23743 }
23744
23745 $user = $this->get_current_or_network_user();
23746
23747 $this->_user_api = FS_Api::instance(
23748 $this->_module_id,
23749 'user',
23750 $user->id,
23751 $user->public_key,
23752 ! $this->is_live(),
23753 $user->secret_key,
23754 $this->get_sdk_version()
23755 );
23756
23757 return $this->_user_api;
23758 }
23759
23760 private $_site_api;
23761
23762 /**
23763 *
23764 * @author Vova Feldman (@svovaf)
23765 * @since 1.0.2
23766 *
23767 * @param bool $flush
23768 *
23769 * @return FS_Api
23770 */
23771 private function get_api_site_scope( $flush = false ) {
23772 if ( ! isset( $this->_site_api ) || $flush ) {
23773 $this->_site_api = FS_Api::instance(
23774 $this->_module_id,
23775 'install',
23776 $this->_site->id,
23777 $this->_site->public_key,
23778 ! $this->is_live(),
23779 $this->_site->secret_key,
23780 $this->get_sdk_version(),
23781 self::get_unfiltered_site_url()
23782 );
23783 }
23784
23785 return $this->_site_api;
23786 }
23787
23788 /**
23789 * @author Leo Fajardo (@leorw)
23790 * @since 2.5.0
23791 *
23792 * @param string $path
23793 * @param string $method
23794 * @param array $params
23795 * @param bool $flush_instance
23796 *
23797 * @return array|mixed|string|void
23798 * @throws Freemius_Exception
23799 */
23800 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23801 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23802
23803 /**
23804 * 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.
23805 *
23806 * @author Leo Fajardo (@leorw)
23807 * @since 2.5.0
23808 */
23809 if (
23810 $this->is_registered() &&
23811 FS_Api::is_api_result_entity( $result ) &&
23812 isset( $result->url )
23813 ) {
23814 $stored_local_url = trailingslashit( $this->_site->url );
23815 $stored_remote_url = trailingslashit( $result->url );
23816
23817 if ( $stored_local_url !== $stored_remote_url ) {
23818 $this->_site->url = $result->url;
23819 $this->_store_site();
23820 }
23821
23822 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23823 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23824 }
23825 }
23826
23827 return $result;
23828 }
23829
23830 private $_plugin_api;
23831
23832 /**
23833 * Get plugin public API scope.
23834 *
23835 * @author Vova Feldman (@svovaf)
23836 * @since 1.0.7
23837 *
23838 * @return FS_Api
23839 */
23840 function get_api_plugin_scope() {
23841 if ( ! isset( $this->_plugin_api ) ) {
23842 $this->_plugin_api = FS_Api::instance(
23843 $this->_module_id,
23844 'plugin',
23845 $this->_plugin->id,
23846 $this->_plugin->public_key,
23847 ! $this->is_live(),
23848 false,
23849 $this->get_sdk_version()
23850 );
23851 }
23852
23853 return $this->_plugin_api;
23854 }
23855
23856 /**
23857 * Get bundle public API scope.
23858 *
23859 * @author Vova Feldman (@svovaf)
23860 * @since 2.3.1
23861 *
23862 * @return FS_Api
23863 */
23864 function get_api_bundle_scope() {
23865 return FS_Api::instance(
23866 $this->get_bundle_id(),
23867 'plugin',
23868 $this->get_bundle_id(),
23869 $this->get_bundle_public_key(),
23870 ! $this->is_live(),
23871 false,
23872 $this->get_sdk_version()
23873 );
23874 }
23875
23876 /**
23877 * Get site API scope object (fallback to public plugin scope when not registered).
23878 *
23879 * @author Vova Feldman (@svovaf)
23880 * @since 1.0.7
23881 *
23882 * @return FS_Api
23883 */
23884 function get_api_site_or_plugin_scope() {
23885 return $this->is_registered() ?
23886 $this->get_api_site_scope() :
23887 $this->get_api_plugin_scope();
23888 }
23889
23890 /**
23891 * @author Leo Fajardo (@leorw)
23892 * @since 2.2.3.1
23893 *
23894 * @param object $result
23895 */
23896 private function maybe_modify_api_curl_error_message( $result ) {
23897 if (
23898 'cUrlMissing' !== $result->error->type &&
23899 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23900 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23901 ) {
23902 return;
23903 }
23904
23905 $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' ) .
23906 ' ' .
23907 $this->esc_html_inline(
23908 sprintf(
23909 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23910 implode(
23911 ', ',
23912 $this->apply_filters( 'api_domains', array(
23913 'api.freemius.com',
23914 'wp.freemius.com'
23915 ) )
23916 )
23917 ),
23918 'connectivity-whitelist'
23919 ) .
23920 ' ' .
23921 sprintf(
23922 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23923 $this->get_module_type()
23924 );
23925 }
23926
23927 /**
23928 * Show trial promotional notice (if any trial exist).
23929 *
23930 * @author Vova Feldman (@svovaf)
23931 * @since 1.0.9
23932 *
23933 * @param FS_Plugin_Plan[] $plans
23934 */
23935 function _check_for_trial_plans( $plans ) {
23936 /**
23937 * 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.
23938 *
23939 * @author Vova Feldman (@svovaf)
23940 * @since 2.1.2
23941 */
23942 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23943 $plans = array( $plans );
23944 }
23945
23946 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23947 $plans = array();
23948 }
23949
23950 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23951 }
23952
23953 /**
23954 * During trial promotion the "upgrade" submenu item turns to
23955 * "start trial" to encourage the trial. Since we want to keep
23956 * the same menu item handler and there's no robust way to
23957 * add new arguments to the menu item link's querystring,
23958 * use JavaScript to find the menu item and update the href of
23959 * the link.
23960 *
23961 * @author Vova Feldman (@svovaf)
23962 * @since 1.2.1.5
23963 */
23964 function _fix_start_trial_menu_item_url() {
23965 $template_args = array( 'id' => $this->_module_id );
23966 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23967 }
23968
23969 /**
23970 * Check if module is currently in a trial promotion mode.
23971 *
23972 * @author Vova Feldman (@svovaf)
23973 * @since 1.2.2.7
23974 *
23975 * @return bool
23976 */
23977 function is_in_trial_promotion() {
23978 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23979 }
23980
23981 /**
23982 * Show trial promotional notice (if any trial exist).
23983 *
23984 * @author Vova Feldman (@svovaf)
23985 * @since 1.0.9
23986 *
23987 * @return bool If trial notice added.
23988 */
23989 function _add_trial_notice() {
23990 if ( ! $this->is_user_admin() ) {
23991 return false;
23992 }
23993
23994 if ( ! $this->is_user_in_admin() ) {
23995 return false;
23996 }
23997
23998 if ( $this->_is_network_active ) {
23999 if ( fs_is_network_admin() ) {
24000 // Network level trial is disabled at the moment.
24001 return false;
24002 }
24003
24004 if ( ! $this->is_delegated_connection() ) {
24005 // Only delegated sites should support trials.
24006 return false;
24007 }
24008 }
24009
24010 // Check if trial message is already shown.
24011 if ( $this->is_in_trial_promotion() ) {
24012 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
24013
24014 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
24015
24016 return false;
24017 }
24018
24019 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
24020 // Don't show trial if running the premium code, unless running in DEV mode.
24021 return false;
24022 }
24023
24024 if ( ! $this->has_trial_plan() ) {
24025 // No plans with trial.
24026 return false;
24027 }
24028
24029 if ( ! $this->apply_filters( 'show_trial', true ) ) {
24030 // Developer explicitly asked not to show the trial promo.
24031 return false;
24032 }
24033
24034 if ( $this->is_registered() ) {
24035 // Check if trial already utilized.
24036 if ( $this->_site->is_trial_utilized() ) {
24037 return false;
24038 }
24039
24040 if ( $this->is_paying_or_trial() ) {
24041 // Don't show trial if paying or already in trial.
24042 return false;
24043 }
24044 }
24045
24046 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24047 // If not yet opted-in/skipped, or pending activation, don't show trial.
24048 return false;
24049 }
24050
24051 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24052 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24053
24054 // Show promotion if never shown before and 24 hours after initial activation with FS.
24055 if ( ! $was_promotion_shown_before &&
24056 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24057 ) {
24058 return false;
24059 }
24060
24061 // OR if promotion was shown before, try showing it every 30 days.
24062 if ( $was_promotion_shown_before &&
24063 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24064 ) {
24065 return false;
24066 }
24067
24068 $trial_period = $this->_trial_days;
24069 $require_payment = $this->_is_trial_require_payment;
24070 $trial_url = $this->get_trial_url();
24071 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24072
24073 if ( $this->is_registered() ) {
24074 // If opted-in, override trial with up to date data from API.
24075 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
24076 $trial_plans_count = count( $trial_plans );
24077
24078 if ( 0 === $trial_plans_count ) {
24079 // If there's no plans with a trial just exit.
24080 return false;
24081 }
24082
24083 /**
24084 * @var FS_Plugin_Plan $paid_plan
24085 */
24086 $paid_plan = $trial_plans[0];
24087 $require_payment = $paid_plan->is_require_subscription;
24088 $trial_period = $paid_plan->trial_period;
24089
24090 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24091
24092 if ( $total_paid_plans !== $trial_plans_count ) {
24093 // Not all paid plans have a trial - generate a string of those that have it.
24094 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24095 $plans_string .= sprintf(
24096 ' <a href="%s">%s</a>',
24097 $trial_url,
24098 $trial_plans[ $i ]->title
24099 );
24100
24101 if ( $i < $trial_plans_count - 2 ) {
24102 $plans_string .= ', ';
24103 } else if ( $i == $trial_plans_count - 2 ) {
24104 $plans_string .= ' and ';
24105 }
24106 }
24107 }
24108 }
24109
24110 $message = sprintf(
24111 $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' ),
24112 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24113 $plans_string,
24114 $trial_period
24115 );
24116
24117 // "No Credit-Card Required" or "No Commitment for N Days".
24118 $cc_string = $require_payment ?
24119 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24120 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24121
24122
24123 // Start trial button.
24124 $button = ' ' . sprintf(
24125 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24126 $trial_url,
24127 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24128 );
24129
24130 $this->_admin_notices->add_sticky(
24131 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
24132 'trial_promotion',
24133 '',
24134 'promotion'
24135 );
24136
24137 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24138
24139 return true;
24140 }
24141
24142 /**
24143 * Lets users/customers know that the product has an affiliate program.
24144 *
24145 * @author Leo Fajardo (@leorw)
24146 * @since 1.2.2.11
24147 *
24148 * @return bool Returns true if the notice has been added.
24149 */
24150 function _add_affiliate_program_notice() {
24151 if ( ! $this->is_user_admin() ) {
24152 return false;
24153 }
24154
24155 if ( ! $this->is_user_in_admin() ) {
24156 return false;
24157 }
24158
24159 // Check if the notice is already shown.
24160 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24161 return false;
24162 }
24163
24164 if (
24165 // Product has no affiliate program.
24166 ! $this->has_affiliate_program() ||
24167 // User has applied for an affiliate account.
24168 ! empty( $this->_storage->affiliate_application_data )
24169 ) {
24170 return false;
24171 }
24172
24173 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24174 // Developer explicitly asked not to show the notice about the affiliate program.
24175 return false;
24176 }
24177
24178 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24179 // If not yet opted in/skipped, or pending activation, don't show the notice.
24180 return false;
24181 }
24182
24183 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24184 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24185
24186 /**
24187 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24188 * activation with FS.
24189 */
24190 if ( $was_notice_shown_before ||
24191 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24192 ) {
24193 return false;
24194 }
24195
24196 if ( ! $this->is_paying() &&
24197 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24198 ) {
24199 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24200 return false;
24201 }
24202
24203 $message = sprintf(
24204 $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' ),
24205 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24206 $this->get_module_label( true )
24207 );
24208
24209 // HTML code for the "Learn more..." button.
24210 $button = ' ' . sprintf(
24211 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24212 $this->_get_admin_page_url( 'affiliation' ),
24213 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24214 );
24215
24216 $this->_admin_notices->add_sticky(
24217 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24218 'affiliate_program',
24219 '',
24220 'promotion'
24221 );
24222
24223 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24224
24225 return true;
24226 }
24227
24228 /**
24229 * @author Vova Feldman (@svovaf)
24230 * @since 1.2.1.5
24231 */
24232 function _enqueue_common_css() {
24233 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24234 // Add basic CSS for admin-notices and menu-item colors.
24235 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24236 }
24237 }
24238
24239 /**
24240 * @author Leo Fajardo (@leorw)
24241 * @since 1.2.2
24242 */
24243 function _show_theme_activation_optin_dialog() {
24244 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24245
24246 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24247 }
24248
24249 /**
24250 * @author Leo Fajardo (@leorw)
24251 * @since 1.2.2
24252 */
24253 function _add_fs_theme_activation_dialog() {
24254 global $pagenow;
24255
24256 if ( 'themes.php' !== $pagenow ) {
24257 return;
24258 }
24259
24260 $vars = array( 'id' => $this->_module_id );
24261 fs_require_once_template( 'connect.php', $vars );
24262 }
24263
24264 /* Action Links
24265 ------------------------------------------------------------------------------------------------------------------*/
24266 private $_action_links_hooked = false;
24267 private $_action_links = array();
24268
24269 /**
24270 * Hook to plugin action links filter.
24271 *
24272 * @author Vova Feldman (@svovaf)
24273 * @since 1.0.0
24274 */
24275 private function hook_plugin_action_links() {
24276 $this->_logger->entrance();
24277
24278 $this->_action_links_hooked = true;
24279
24280 $this->_logger->log( 'Adding action links hooks.' );
24281
24282 // Add action link to settings page.
24283 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24284 &$this,
24285 '_modify_plugin_action_links_hook'
24286 ), WP_FS__DEFAULT_PRIORITY, 2 );
24287 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24288 &$this,
24289 '_modify_plugin_action_links_hook'
24290 ), WP_FS__DEFAULT_PRIORITY, 2 );
24291 }
24292
24293 /**
24294 * Add plugin action link.
24295 *
24296 * @author Vova Feldman (@svovaf)
24297 * @since 1.0.0
24298 *
24299 * @param $label
24300 * @param $url
24301 * @param bool $external
24302 * @param int $priority
24303 * @param bool $key
24304 */
24305 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24306 $this->_logger->entrance();
24307
24308 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24309 $this->_action_links[ $priority ] = array();
24310 }
24311
24312 if ( false === $key ) {
24313 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24314 }
24315
24316 $this->_action_links[ $priority ][] = array(
24317 'label' => $label,
24318 'href' => $url,
24319 'key' => $key,
24320 'external' => $external
24321 );
24322 }
24323
24324 /**
24325 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24326 *
24327 * @author Vova Feldman (@svovaf)
24328 * @since 1.0.0
24329 */
24330 function _add_upgrade_action_link() {
24331 $this->_logger->entrance();
24332
24333 $is_activation_mode = $this->is_activation_mode();
24334
24335 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24336
24337 /**
24338 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24339 * and "Add-Ons" menus should be added.
24340 *
24341 * @author Leo Fajardo (@leorw)
24342 * @since 2.3.0
24343 */
24344 $add_upgrade_link = (
24345 $add_action_links ||
24346 ( $is_activation_mode && $this->is_only_premium() )
24347 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24348
24349 $add_addons_link = ( $add_action_links && $this->has_addons() );
24350
24351 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24352 return;
24353 }
24354
24355 if (
24356 $add_upgrade_link &&
24357 $this->is_pricing_page_visible() &&
24358 $this->is_submenu_item_visible( 'pricing' )
24359 ) {
24360 $this->add_plugin_action_link(
24361 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24362 $this->get_upgrade_url(),
24363 false,
24364 7,
24365 'upgrade'
24366 );
24367 }
24368
24369 if (
24370 $add_addons_link &&
24371 $this->has_addons() &&
24372 $this->is_submenu_item_visible( 'addons' )
24373 ) {
24374 $this->add_plugin_action_link(
24375 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24376 $this->_get_admin_page_url( 'addons' ),
24377 false,
24378 9,
24379 'addons'
24380 );
24381 }
24382 }
24383
24384 /**
24385 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24386 *
24387 * @author Leo Fajardo (@leorw)
24388 * @since 1.1.9
24389 */
24390 function _add_license_action_link() {
24391 $this->_logger->entrance();
24392
24393 if ( ! self::is_ajax() ) {
24394 // Inject license activation dialog UI and client side code.
24395 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24396 }
24397
24398 $link_text = $this->is_free_plan() ?
24399 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24400 $this->get_text_inline( 'Change License', 'change-license' );
24401
24402 $this->add_plugin_action_link(
24403 $link_text,
24404 '#',
24405 false,
24406 11,
24407 ( 'activate-license ' . $this->get_unique_affix() )
24408 );
24409 }
24410
24411 /**
24412 * @author Leo Fajardo (@leorw)
24413 * @since 2.0.2
24414 */
24415 function _add_premium_version_upgrade_selection_action() {
24416 $this->_logger->entrance();
24417
24418 if ( ! self::is_ajax() ) {
24419 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24420 }
24421 }
24422
24423 /**
24424 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24425 *
24426 * @author Leo Fajardo (@leorw)
24427 * @since 1.2.1.5
24428 */
24429 function _add_tracking_links() {
24430 if ( ! current_user_can( 'manage_options' ) ) {
24431 return;
24432 }
24433
24434 $this->_logger->entrance();
24435
24436 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24437 // 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.
24438 return;
24439 }
24440
24441 if (
24442 $this->is_addon() &&
24443 ! $this->is_only_premium()
24444 ) {
24445 $parent = $this->get_parent_instance();
24446
24447 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24448 return;
24449 }
24450 }
24451
24452 if ( fs_is_network_admin() ) {
24453 if ( ! $this->_is_network_active ) {
24454 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24455 return;
24456 } else if ( $this->is_network_delegated_connection() ) {
24457 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24458 return;
24459 }
24460 } else {
24461 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24462 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24463 return;
24464 }
24465 }
24466
24467 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24468 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24469 $this->connect_again();
24470
24471 return;
24472 }
24473 }
24474
24475 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24476 ( $this->is_theme() && ! self::is_themes_page() )
24477 ) {
24478 // Only show tracking links on the plugins and themes pages.
24479 return;
24480 }
24481
24482 if (
24483 $this->is_activation_mode() &&
24484 $this->is_premium() &&
24485 ! $this->is_registered()
24486 ) {
24487 // If not yet registered and running the premium code base, a license activation link will already be shown.
24488 return;
24489 }
24490
24491 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24492 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24493 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24494 return;
24495 }
24496 }
24497
24498 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24499 return;
24500 }
24501
24502 $link_text_id = '';
24503 $url = '#';
24504
24505 if ( $this->is_registered( true ) ) {
24506 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24507 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24508 } else {
24509 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24510 }
24511 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24512 /**
24513 * Show opt-in link only if skipped or in activation mode.
24514 */
24515 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24516
24517 $params = ! $this->is_anonymous() ?
24518 array() :
24519 array(
24520 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24521 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24522 );
24523
24524 $url = $this->get_activation_url( $params );
24525 }
24526
24527 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24528
24529 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24530 $this->add_plugin_action_link(
24531 $link_text_id,
24532 $url,
24533 false,
24534 13,
24535 "opt-in-or-opt-out {$this->_slug}"
24536 );
24537 }
24538 }
24539
24540 /**
24541 * Get the URL of the page that should be loaded right after the plugin activation.
24542 *
24543 * @author Vova Feldman (@svovaf)
24544 * @since 1.1.7.4
24545 *
24546 * @return string
24547 */
24548 function get_after_plugin_activation_redirect_url() {
24549 $url = false;
24550
24551 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24552 $first_time_path = $this->_menu->get_first_time_path(
24553 fs_is_network_admin() && $this->_is_network_active
24554 );
24555
24556 if ( $this->is_activation_mode() ) {
24557 $url = $this->get_activation_url();
24558 } else if ( ! empty( $first_time_path ) ) {
24559 $url = $first_time_path;
24560 } else {
24561 $page = '';
24562 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24563 if ( $this->is_network_registered() ) {
24564 $page = 'account';
24565 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24566 $this->maybe_set_slug_and_network_menu_exists_flag();
24567 }
24568 }
24569
24570 $url = $this->_get_admin_page_url( $page );
24571 }
24572 } else {
24573 $plugin_fs = false;
24574
24575 if ( $this->is_parent_plugin_installed() ) {
24576 $plugin_fs = self::get_parent_instance();
24577 }
24578
24579 if ( is_object( $plugin_fs ) ) {
24580 if ( ! $plugin_fs->is_registered() ) {
24581 // Forward to parent plugin connect when parent not registered.
24582 $url = $plugin_fs->get_activation_url();
24583 } else {
24584 // Forward to account page.
24585 $url = $plugin_fs->_get_admin_page_url( 'account' );
24586 }
24587 }
24588 }
24589
24590 return $url;
24591 }
24592
24593 /**
24594 * Forward page to activation page.
24595 *
24596 * @author Vova Feldman (@svovaf)
24597 * @since 1.0.3
24598 */
24599 function _redirect_on_activation_hook() {
24600 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24601 $url = $this->get_after_plugin_activation_redirect_url();
24602
24603 if ( is_string( $url ) ) {
24604 fs_redirect( $url );
24605 }
24606 }
24607 }
24608
24609 /**
24610 * Modify plugin's page action links collection.
24611 *
24612 * @author Vova Feldman (@svovaf)
24613 * @since 1.0.0
24614 *
24615 * @param array $links
24616 * @param $file
24617 *
24618 * @return array
24619 */
24620 function _modify_plugin_action_links_hook( $links, $file ) {
24621 $this->_logger->entrance();
24622
24623 $passed_deactivate = false;
24624 $deactivate_link = '';
24625 $before_deactivate = array();
24626 $after_deactivate = array();
24627 foreach ( $links as $key => $link ) {
24628 if ( 'deactivate' === $key ) {
24629 $deactivate_link = $link;
24630 $passed_deactivate = true;
24631 continue;
24632 }
24633
24634 if ( ! $passed_deactivate ) {
24635 $before_deactivate[ $key ] = $link;
24636 } else {
24637 $after_deactivate[ $key ] = $link;
24638 }
24639 }
24640
24641 ksort( $this->_action_links );
24642
24643 foreach ( $this->_action_links as $new_links ) {
24644 foreach ( $new_links as $link ) {
24645 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24646 }
24647 }
24648
24649 if ( ! empty( $deactivate_link ) ) {
24650 /**
24651 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24652 *
24653 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24654 */
24655 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24656
24657 // Append deactivation link.
24658 $before_deactivate['deactivate'] = $deactivate_link;
24659 }
24660
24661 return array_merge( $before_deactivate, $after_deactivate );
24662 }
24663
24664 /**
24665 * Adds admin message.
24666 *
24667 * @author Vova Feldman (@svovaf)
24668 * @since 1.0.4
24669 *
24670 * @param string $message
24671 * @param string $title
24672 * @param string $type
24673 */
24674 function add_admin_message( $message, $title = '', $type = 'success' ) {
24675 $this->_admin_notices->add( $message, $title, $type );
24676 }
24677
24678 /**
24679 * Adds sticky admin message.
24680 *
24681 * @author Vova Feldman (@svovaf)
24682 * @since 1.1.0
24683 *
24684 * @param string $message
24685 * @param string $id
24686 * @param string $title
24687 * @param string $type
24688 */
24689 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24690 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24691 }
24692
24693 /**
24694 * Check if the paid version of the module is installed.
24695 *
24696 * @author Vova Feldman (@svovaf)
24697 * @since 2.2.0
24698 *
24699 * @return bool
24700 */
24701 private function is_premium_version_installed() {
24702 $premium_plugin_basename = $this->premium_plugin_basename();
24703
24704 if ( $this->is_theme() ) {
24705 return $this->can_activate_theme( $this->get_premium_slug() );
24706 }
24707
24708 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24709 }
24710
24711 /**
24712 * Helper function that returns the final steps for the upgrade completion.
24713 *
24714 * If the module is already running the premium code, returns an empty string.
24715 *
24716 * @author Vova Feldman (@svovaf)
24717 * @since 1.2.1
24718 *
24719 * @param string $plan_title
24720 *
24721 * @return string
24722 */
24723 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24724 $this->_logger->entrance();
24725
24726 $activate_license_string = $this->get_license_network_activation_notice();
24727
24728 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24729 return '' . $activate_license_string;
24730 }
24731
24732 if ( empty( $plan_title ) ) {
24733 $plan_title = $this->get_plan_title();
24734 }
24735
24736 if ( $this->is_premium_version_installed() ) {
24737 /**
24738 * If the premium version is already installed, instead of showing the installation instructions,
24739 * tell the current user to activate it.
24740 *
24741 * @author Leo Fajardo (@leorw)
24742 * @since 2.2.1
24743 */
24744 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24745 $this->get_premium_slug() :
24746 $this->premium_plugin_basename();
24747
24748 return sprintf(
24749 /* translators: %1$s: Product title; %2$s: Plan title */
24750 $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' ),
24751 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24752 $plan_title,
24753 sprintf(
24754 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24755 ( $this->is_theme() ?
24756 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24757 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24758 esc_html( sprintf(
24759 /* translators: %s: Plan title */
24760 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24761 $plan_title
24762 ) )
24763 )
24764 );
24765 } else {
24766 // @since 1.2.1.5 The free version is auto deactivated.
24767 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24768 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24769 '';
24770
24771 return sprintf(
24772 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24773 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24774 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24775 $this->get_latest_download_link( sprintf(
24776 /* translators: %s: Plan title */
24777 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24778 $plan_title
24779 ) ),
24780 $deactivation_step,
24781 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24782 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24783 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24784 );
24785 }
24786 }
24787
24788 /**
24789 * @author Leo Fajardo (@leorw)
24790 * @since 2.5.3
24791 *
24792 * @param string $message_before_the_instructions
24793 * @param string $message_id
24794 * @param string $plan_title
24795 */
24796 private function add_complete_upgrade_instructions_notice(
24797 $message_before_the_instructions,
24798 $message_id,
24799 $plan_title = ''
24800 ) {
24801 $this->_admin_notices->add_sticky(
24802 $message_before_the_instructions .
24803 $this->get_complete_upgrade_instructions( $plan_title ),
24804 $message_id,
24805 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24806 );
24807 }
24808
24809 /**
24810 * @author Leo Fajardo (@leorw)
24811 * @since 2.5.3
24812 *
24813 * @param bool $is_upgrade
24814 */
24815 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24816 $this->add_complete_upgrade_instructions_notice(
24817 $is_upgrade ?
24818 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24819 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24820 'plan_upgraded'
24821 );
24822 }
24823
24824 /**
24825 * @author Leo Fajardo (@leorw)
24826 * @since 2.1.0
24827 *
24828 * @param string $url
24829 * @param array $request
24830 */
24831 private static function enrich_request_for_debug( &$url, &$request ) {
24832 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
24833 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
24834 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
24835
24836 $request['cookies'] = array(
24837 new WP_Http_Cookie( array(
24838 'name' => 'XDEBUG_SESSION',
24839 'value' => 'PHPSTORM',
24840 ) )
24841 );
24842 }
24843 }
24844
24845 /**
24846 * @author Leo Fajardo (@leorw)
24847 * @since 2.1.0
24848 *
24849 * @param string $url
24850 * @param array $request
24851 * @param int $success_cache_expiration
24852 * @param int $failure_cache_expiration
24853 * @param bool $maybe_enrich_request_for_debug
24854 *
24855 * @return WP_Error|array
24856 */
24857 static function safe_remote_post(
24858 &$url,
24859 $request,
24860 $success_cache_expiration = 0,
24861 $failure_cache_expiration = 0,
24862 $maybe_enrich_request_for_debug = true
24863 ) {
24864 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24865
24866 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24867
24868 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24869 get_transient( $cache_key ) :
24870 false;
24871
24872 if ( false === $response ) {
24873 if ( $maybe_enrich_request_for_debug ) {
24874 self::enrich_request_for_debug( $url, $request );
24875 }
24876
24877 if ( ! isset( $request['method'] ) ) {
24878 $request['method'] = 'POST';
24879 }
24880
24881 $response = FS_Api::remote_request( $url, $request );
24882
24883 if (
24884 'https://' === substr( $url, 0, 8 ) &&
24885 FS_Api::is_ssl_error_response( $response )
24886 ) {
24887 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24888 $url = 'http://' . substr( $url, 8 );
24889
24890 $request['timeout'] = 15;
24891 $response = FS_Api::remote_request( $url, $request );
24892 }
24893
24894 if ( false !== $cache_key ) {
24895 set_transient(
24896 $cache_key,
24897 $response,
24898 ( ( $response instanceof WP_Error ) ?
24899 $failure_cache_expiration :
24900 $success_cache_expiration )
24901 );
24902 }
24903 }
24904
24905 return $response;
24906 }
24907
24908 /**
24909 * This method is used to enrich the after upgrade notice instructions when the upgraded
24910 * license cannot be activated network wide (license quota isn't large enough).
24911 *
24912 * @author Vova Feldman (@svovaf)
24913 * @since 2.0.0
24914 *
24915 * @return string
24916 */
24917 private function get_license_network_activation_notice() {
24918 if ( ! $this->_is_network_active ) {
24919 // Module isn't network level activated.
24920 return '';
24921 }
24922
24923 if ( ! fs_is_network_admin() ) {
24924 // Not network level admin.
24925 return '';
24926 }
24927
24928 if ( get_blog_count() == 1 ) {
24929 // There's only a single site in the network so if there's a context license it was already activated.
24930 return '';
24931 }
24932
24933 if ( ! is_object( $this->_license ) ) {
24934 // No context license.
24935 return '';
24936 }
24937
24938 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24939 // 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).
24940 return '';
24941 }
24942
24943 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24944 // 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).
24945 return '';
24946 }
24947
24948 return sprintf(
24949 $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' ),
24950 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24951 '</a>'
24952 );
24953 }
24954
24955 /**
24956 * @author Vova Feldman (@svovaf)
24957 * @since 1.2.1.7
24958 *
24959 * @param string $key
24960 *
24961 * @return string
24962 */
24963 function get_text( $key ) {
24964 return fs_text( $key, $this->_slug );
24965 }
24966
24967 /**
24968 * @author Vova Feldman (@svovaf)
24969 * @since 1.2.3
24970 *
24971 * @param string $text Translatable string.
24972 * @param string $key String key for overrides.
24973 *
24974 * @return string
24975 */
24976 function get_text_inline( $text, $key = '' ) {
24977 return _fs_text_inline( $text, $key, $this->_slug );
24978 }
24979
24980 /**
24981 * @author Vova Feldman (@svovaf)
24982 * @since 1.2.3
24983 *
24984 * @param string $text Translatable string.
24985 * @param string $context Context information for the translators.
24986 * @param string $key String key for overrides.
24987 *
24988 * @return string
24989 */
24990 function get_text_x_inline( $text, $context, $key ) {
24991 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24992 }
24993
24994 /**
24995 * @author Vova Feldman (@svovaf)
24996 * @since 1.2.3
24997 *
24998 * @param string $text Translatable string.
24999 * @param string $key String key for overrides.
25000 *
25001 * @return string
25002 */
25003 function esc_html_inline( $text, $key ) {
25004 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
25005 }
25006
25007 #----------------------------------------------------------------------------------
25008 #region Versioning
25009 #----------------------------------------------------------------------------------
25010
25011 /**
25012 * Check if Freemius in SDK upgrade mode.
25013 *
25014 * @author Vova Feldman (@svovaf)
25015 * @since 1.0.9
25016 *
25017 * @return bool
25018 */
25019 function is_sdk_upgrade_mode() {
25020 return isset( $this->_storage->sdk_upgrade_mode ) ?
25021 $this->_storage->sdk_upgrade_mode :
25022 false;
25023 }
25024
25025 /**
25026 * Turn SDK upgrade mode off.
25027 *
25028 * @author Vova Feldman (@svovaf)
25029 * @since 1.0.9
25030 */
25031 function set_sdk_upgrade_complete() {
25032 $this->_storage->sdk_upgrade_mode = false;
25033 }
25034
25035 /**
25036 * Check if plugin upgrade mode.
25037 *
25038 * @author Vova Feldman (@svovaf)
25039 * @since 1.0.9
25040 *
25041 * @return bool
25042 */
25043 function is_plugin_upgrade_mode() {
25044 return isset( $this->_storage->plugin_upgrade_mode ) ?
25045 $this->_storage->plugin_upgrade_mode :
25046 false;
25047 }
25048
25049 /**
25050 * Turn plugin upgrade mode off.
25051 *
25052 * @author Vova Feldman (@svovaf)
25053 * @since 1.0.9
25054 */
25055 function set_plugin_upgrade_complete() {
25056 $this->_storage->plugin_upgrade_mode = false;
25057
25058 $license_migration = ! empty( $this->_storage->license_migration ) ?
25059 $this->_storage->license_migration :
25060 array();
25061
25062 $license_migration['is_migrating'] = false;
25063
25064 $this->_storage->license_migration = $license_migration;
25065 }
25066
25067 #endregion
25068
25069 #----------------------------------------------------------------------------------
25070 #region Permissions
25071 #----------------------------------------------------------------------------------
25072
25073 /**
25074 * Check if specific permission requested.
25075 *
25076 * @author Vova Feldman (@svovaf)
25077 * @since 1.1.6
25078 *
25079 * @param string $permission
25080 *
25081 * @return bool
25082 */
25083 function is_permission_requested( $permission ) {
25084 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25085 }
25086
25087 #endregion
25088
25089 #----------------------------------------------------------------------------------
25090 #region Auto Activation
25091 #----------------------------------------------------------------------------------
25092
25093 /**
25094 * Hints the SDK if running an auto-installation.
25095 *
25096 * @var bool
25097 */
25098 private $_isAutoInstall = false;
25099
25100 /**
25101 * After upgrade callback to install and auto activate a plugin.
25102 * This code will only be executed on explicit request from the user,
25103 * following the practice Jetpack are using with their theme installations.
25104 *
25105 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25106 *
25107 * @author Vova Feldman (@svovaf)
25108 * @since 1.2.1.7
25109 */
25110 function _install_premium_version_ajax_action() {
25111 $this->_logger->entrance();
25112
25113 $this->check_ajax_referer( 'install_premium_version' );
25114
25115 if ( ! $this->is_registered() ) {
25116 // Not registered.
25117 self::shoot_ajax_failure( array(
25118 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25119 'code' => 'premium_installed',
25120 ) );
25121 }
25122
25123 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25124
25125 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25126 // Invalid ID.
25127 self::shoot_ajax_failure( array(
25128 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25129 'code' => 'invalid_module_id',
25130 ) );
25131 }
25132
25133 if ( $plugin_id == $this->get_id() ) {
25134 if ( $this->is_premium() ) {
25135 // Already using the premium code version.
25136 self::shoot_ajax_failure( array(
25137 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25138 'code' => 'premium_installed',
25139 ) );
25140 }
25141 if ( ! $this->can_use_premium_code() ) {
25142 // Don't have access to the premium code.
25143 self::shoot_ajax_failure( array(
25144 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25145 'code' => 'invalid_license',
25146 ) );
25147 }
25148 if ( ! $this->has_release_on_freemius() ) {
25149 // Plugin is a serviceware, no premium code version.
25150 self::shoot_ajax_failure( array(
25151 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25152 'code' => 'premium_version_missing',
25153 ) );
25154 }
25155 } else {
25156 $addon = $this->get_addon( $plugin_id );
25157
25158 if ( ! is_object( $addon ) ) {
25159 // Invalid add-on ID.
25160 self::shoot_ajax_failure( array(
25161 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25162 'code' => 'invalid_module_id',
25163 ) );
25164 }
25165
25166 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25167 // Premium add-on version is already activated.
25168 self::shoot_ajax_failure( array(
25169 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25170 'code' => 'premium_installed',
25171 ) );
25172 }
25173 }
25174
25175 $this->_isAutoInstall = true;
25176
25177 // Try to install and activate.
25178 $updater = FS_Plugin_Updater::instance( $this );
25179 $result = $updater->install_and_activate_plugin( $plugin_id );
25180
25181 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25182 self::shoot_ajax_failure( array(
25183 'message' => $result['message'],
25184 'code' => $result['code'],
25185 ) );
25186 }
25187
25188 self::shoot_ajax_success( $result );
25189 }
25190
25191 /**
25192 * Displays module activation dialog box after a successful upgrade
25193 * where the user explicitly requested to auto download and install
25194 * the premium version.
25195 *
25196 * @author Vova Feldman (@svovaf)
25197 * @since 1.2.1.7
25198 */
25199 function _add_auto_installation_dialog_box() {
25200 $this->_logger->entrance();
25201
25202 if ( ! $this->is_registered() ) {
25203 // Not registered.
25204 return;
25205 }
25206
25207 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25208
25209 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25210 // Invalid module ID.
25211 return;
25212 }
25213
25214 if ( $plugin_id == $this->get_id() ) {
25215 if ( $this->is_premium() ) {
25216 // Already using the premium code version.
25217 return;
25218 }
25219 if ( ! $this->can_use_premium_code() ) {
25220 // Don't have access to the premium code.
25221 return;
25222 }
25223 if ( ! $this->has_release_on_freemius() ) {
25224 // Plugin is a serviceware, no premium code version.
25225 return;
25226 }
25227 } else {
25228 $addon = $this->get_addon( $plugin_id );
25229
25230 if ( ! is_object( $addon ) ) {
25231 // Invalid add-on ID.
25232 return;
25233 }
25234
25235 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25236 // Premium add-on version is already activated.
25237 return;
25238 }
25239 }
25240
25241 $vars = array(
25242 'id' => $this->_module_id,
25243 'target_module_id' => $plugin_id,
25244 'slug' => $this->_slug,
25245 );
25246
25247 fs_require_template( 'auto-installation.php', $vars );
25248 }
25249
25250 #endregion
25251
25252 #--------------------------------------------------------------------------------
25253 #region Tabs Integration
25254 #--------------------------------------------------------------------------------
25255
25256 #region Module's Original Tabs
25257
25258 /**
25259 * Inject a JavaScript logic to capture the theme tabs HTML.
25260 *
25261 * @author Vova Feldman (@svovaf)
25262 * @since 1.2.2.7
25263 */
25264 function _tabs_capture() {
25265 $this->_logger->entrance();
25266
25267 if (
25268 ! $this->is_product_settings_page() ||
25269 ! $this->should_page_include_tabs() ||
25270 ! $this->is_matching_url( $this->main_menu_url() )
25271 ) {
25272 return;
25273 }
25274
25275 $params = array(
25276 'id' => $this->_module_id,
25277 );
25278
25279 fs_require_once_template( 'tabs-capture-js.php', $params );
25280 }
25281
25282 /**
25283 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25284 * after version and type (free/premium) changes, in addition to the week period.
25285 *
25286 * @author Vova Feldman (@svovaf)
25287 * @since 1.2.2.7
25288 */
25289 function _store_tabs_ajax_action() {
25290 $this->_logger->entrance();
25291
25292 $this->check_ajax_referer( 'store_tabs' );
25293
25294 // Init filesystem if not yet initiated.
25295 WP_Filesystem();
25296
25297 // Get POST body HTML data.
25298 global $wp_filesystem;
25299 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25300
25301 if ( is_string( $tabs_html ) ) {
25302 $tabs_html = trim( $tabs_html );
25303 }
25304
25305 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25306 self::shoot_ajax_failure();
25307 }
25308
25309 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25310
25311 self::shoot_ajax_success();
25312 }
25313
25314 /**
25315 * Cache theme's settings page custom styles. The cache will also be set as expired
25316 * after version and type (free/premium) changes, in addition to the week period.
25317 *
25318 * @author Vova Feldman (@svovaf)
25319 * @since 1.2.2.7
25320 */
25321 function _store_tabs_styles() {
25322 $this->_logger->entrance();
25323
25324 if (
25325 ! $this->is_product_settings_page() ||
25326 ! $this->should_page_include_tabs() ||
25327 ! $this->is_matching_url( $this->main_menu_url() )
25328 ) {
25329 return;
25330 }
25331
25332 $wp_styles = wp_styles();
25333
25334 $theme_styles_url = get_template_directory_uri();
25335
25336 $stylesheets = array();
25337 foreach ( $wp_styles->queue as $handler ) {
25338 if ( fs_starts_with( $handler, 'fs_' ) ) {
25339 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25340 continue;
25341 }
25342
25343 /**
25344 * @var _WP_Dependency $stylesheet
25345 */
25346 $stylesheet = $wp_styles->registered[ $handler ];
25347
25348 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25349 $stylesheets[] = $stylesheet->src;
25350 }
25351 }
25352
25353 if ( ! empty( $stylesheets ) ) {
25354 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25355 }
25356 }
25357
25358 /**
25359 * Check if module's original settings page has any tabs.
25360 *
25361 * @author Vova Feldman (@svovaf)
25362 * @since 1.2.2.7
25363 *
25364 * @return bool
25365 */
25366 private function has_tabs() {
25367 return $this->_cache->has( 'tabs' );
25368 }
25369
25370 /**
25371 * Get module's settings page HTML content, starting
25372 * from the beginning of the <div class="wrap"> element,
25373 * until the tabs HTML (including).
25374 *
25375 * @author Vova Feldman (@svovaf)
25376 * @since 1.2.2.7
25377 *
25378 * @return string
25379 */
25380 private function get_tabs_html() {
25381 $this->_logger->entrance();
25382
25383 return $this->_cache->get( 'tabs' );
25384 }
25385
25386 /**
25387 * Check if page should include tabs.
25388 *
25389 * @author Vova Feldman (@svovaf)
25390 * @since 1.2.2.7
25391 *
25392 * @return bool
25393 */
25394 private function should_page_include_tabs() {
25395 if ( ! $this->has_settings_menu() ) {
25396 // Don't add tabs if no settings at all.
25397 return false;
25398 }
25399
25400 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25401 // Only add tabs to themes for now.
25402 return false;
25403 }
25404
25405 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25406 // Only add tabs to monetizing themes.
25407 return false;
25408 }
25409
25410 if ( ! $this->is_product_settings_page() ) {
25411 // Only add tabs if browsing one of the product's setting pages.
25412 return false;
25413 }
25414
25415 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25416 // Don't include tabs in the activation page.
25417 return false;
25418 }
25419
25420 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25421 // Don't add tabs on checkout page, we want to reduce distractions
25422 // as much as possible.
25423 return false;
25424 }
25425
25426 return true;
25427 }
25428
25429 /**
25430 * Add the tabs HTML before the setting's page content and
25431 * enqueue any required stylesheets.
25432 *
25433 * @author Vova Feldman (@svovaf)
25434 * @since 1.2.2.7
25435 *
25436 * @return bool If tabs were included.
25437 */
25438 function _add_tabs_before_content() {
25439 $this->_logger->entrance();
25440
25441 if ( ! $this->should_page_include_tabs() ) {
25442 return false;
25443 }
25444
25445 /**
25446 * Enqueue the original stylesheets that are included in the
25447 * theme settings page. That way, if the theme settings has
25448 * some custom _styled_ content above the tabs UI, this
25449 * will make sure that the styling is preserved.
25450 */
25451 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25452 if ( is_array( $stylesheets ) ) {
25453 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25454 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25455 }
25456 }
25457
25458 // Cut closing </div> tag.
25459 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
25460
25461 return true;
25462 }
25463
25464 /**
25465 * Add the tabs closing HTML after the setting's page content.
25466 *
25467 * @author Vova Feldman (@svovaf)
25468 * @since 1.2.2.7
25469 *
25470 * @return bool If tabs closing HTML was included.
25471 */
25472 function _add_tabs_after_content() {
25473 $this->_logger->entrance();
25474
25475 if ( ! $this->should_page_include_tabs() ) {
25476 return false;
25477 }
25478
25479 echo '</div>';
25480
25481 return true;
25482 }
25483
25484 #endregion
25485
25486 /**
25487 * Add in-page JavaScript to inject the Freemius tabs into
25488 * the module's setting tabs section.
25489 *
25490 * @author Vova Feldman (@svovaf)
25491 * @since 1.2.2.7
25492 */
25493 function _add_freemius_tabs() {
25494 $this->_logger->entrance();
25495
25496 if ( ! $this->should_page_include_tabs() ) {
25497 return;
25498 }
25499
25500 $params = array( 'id' => $this->_module_id );
25501 fs_require_once_template( 'tabs.php', $params );
25502 }
25503
25504 #endregion
25505
25506 #--------------------------------------------------------------------------------
25507 #region Customizer Integration for Themes
25508 #--------------------------------------------------------------------------------
25509
25510 /**
25511 * @author Vova Feldman (@svovaf)
25512 * @since 1.2.2.7
25513 *
25514 * @param WP_Customize_Manager $customizer
25515 */
25516 function _customizer_register( $customizer ) {
25517 $this->_logger->entrance();
25518
25519 if ( $this->is_pricing_page_visible() ) {
25520 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25521
25522 $customizer->add_section( 'freemius_upsell', array(
25523 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25524 'priority' => 1,
25525 ) );
25526 $customizer->add_setting( 'freemius_upsell', array(
25527 'sanitize_callback' => 'esc_html',
25528 ) );
25529
25530 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25531 'fs' => $this,
25532 'section' => 'freemius_upsell',
25533 'priority' => 100,
25534 ) ) );
25535 }
25536
25537 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25538 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25539
25540 // Main Documentation Link In Customizer Root.
25541 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25542 'fs' => $this,
25543 'priority' => 1000,
25544 ) ) );
25545 }
25546 }
25547
25548 #endregion
25549
25550 /**
25551 * If the theme has a paid version, add some custom
25552 * styling to the theme's premium version (if exists)
25553 * to highlight that it's the premium version of the
25554 * same theme, making it easier for identification
25555 * after the user upgrades and upload it to the site.
25556 *
25557 * @author Vova Feldman (@svovaf)
25558 * @since 1.2.2.7
25559 */
25560 function _style_premium_theme() {
25561 $this->_logger->entrance();
25562
25563 if ( ! self::is_themes_page() ) {
25564 // Only include in the themes page.
25565 return;
25566 }
25567
25568 if ( ! $this->has_paid_plan() ) {
25569 // Only include if has any paid plans.
25570 return;
25571 }
25572
25573 $params = null;
25574 fs_require_once_template( '/js/jquery.content-change.php', $params );
25575
25576 $params = array(
25577 'slug' => $this->_slug,
25578 'id' => $this->_module_id,
25579 );
25580
25581 fs_require_template( '/js/style-premium-theme.php', $params );
25582 }
25583
25584 /**
25585 * This method will return the absolute URL of the module's local icon.
25586 *
25587 * When you are running your plugin or theme on a **localhost** environment, if the icon
25588 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25589 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25590 * If an icon is found, this method will automatically attempt to download the icon and store it
25591 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25592 *
25593 * It's important to mention that this method is NOT phoning home since the developer will deploy
25594 * the product with the local icon in the assets folder. The download process just simplifies
25595 * the process for the developer.
25596 *
25597 * @author Vova Feldman (@svovaf)
25598 * @since 2.0.0
25599 *
25600 * @return string
25601 */
25602 function get_local_icon_url() {
25603 global $fs_active_plugins;
25604
25605 /**
25606 * @since 1.1.7.5
25607 */
25608 $local_path = $this->apply_filters( 'plugin_icon', false );
25609
25610 if ( is_string( $local_path ) ) {
25611 $icons = array( $local_path );
25612 } else {
25613 $img_dir = WP_FS__DIR_IMG;
25614
25615 // Locate the main assets folder.
25616 if ( 1 < count( $fs_active_plugins->plugins ) ) {
25617 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25618
25619 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25620 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25621 $img_dir = $plugin_or_theme_img_dir
25622 . '/'
25623 /**
25624 * The basename will be `themes` or the basename of a custom themes directory.
25625 *
25626 * @author Leo Fajardo (@leorw)
25627 * @since 2.2.3
25628 */
25629 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25630 . '/assets/img';
25631
25632 break;
25633 }
25634 }
25635 }
25636
25637 // Try to locate the icon in the assets folder.
25638 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25639
25640 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25641 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25642 $icons = array(
25643 fs_normalize_path( $img_dir . '/theme-icon.png' )
25644 );
25645 } else {
25646 $icon_found = false;
25647 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25648
25649 if ( ! function_exists( 'get_filesystem_method' ) ) {
25650 require_once ABSPATH . 'wp-admin/includes/file.php';
25651 }
25652
25653 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25654
25655 /**
25656 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25657 *
25658 * This code will only be executed once during the testing
25659 * of the plugin in a local environment. The plugin icon file WILL
25660 * already exist in the assets folder when the plugin is deployed to
25661 * the repository.
25662 */
25663 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25664 // Fetch icon from Freemius.
25665 $icon = $this->fetch_remote_icon_url();
25666
25667 // Fetch icon from WordPress.org.
25668 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25669 if ( ! function_exists( 'plugins_api' ) ) {
25670 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25671 }
25672
25673 $plugin_information = plugins_api( 'plugin_information', array(
25674 'slug' => $this->_slug,
25675 'fields' => array(
25676 'sections' => false,
25677 'tags' => false,
25678 'icons' => true
25679 )
25680 ) );
25681
25682 if (
25683 ! is_wp_error( $plugin_information )
25684 && isset( $plugin_information->icons )
25685 && ! empty( $plugin_information->icons )
25686 ) {
25687 /**
25688 * Get the smallest icon.
25689 *
25690 * @author Leo Fajardo (@leorw)
25691 * @since 1.2.2
25692 */
25693 $icon = end( $plugin_information->icons );
25694 }
25695 }
25696
25697 if ( ! empty( $icon ) ) {
25698 if ( 0 !== strpos( $icon, 'http' ) ) {
25699 $icon = 'http:' . $icon;
25700 }
25701
25702 /**
25703 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25704 *
25705 * @author Leo Fajardo (@leorw)
25706 * @since 1.2.2
25707 */
25708 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25709
25710 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25711
25712 // Try to download the icon.
25713 $icon_found = fs_download_image( $icon, $local_path );
25714 }
25715 }
25716
25717 if ( ! $icon_found ) {
25718 // No icons found, fallback to default icon.
25719 if ( $have_write_permissions ) {
25720 // If have write permissions, copy default icon.
25721 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25722 } else {
25723 // If doesn't have write permissions, use default icon path.
25724 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25725 }
25726 }
25727
25728 $icons = array( $local_path );
25729 }
25730 }
25731 }
25732
25733 $icon_dir = dirname( $icons[0] );
25734
25735 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25736 }
25737
25738 /**
25739 * Fetch module's extended info.
25740 *
25741 * @author Vova Feldman (@svovaf)
25742 * @since 2.0.0
25743 *
25744 * @return object|mixed
25745 */
25746 private function fetch_module_info() {
25747 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25748 }
25749
25750 /**
25751 * Fetch module's remote icon URL.
25752 *
25753 * @author Vova Feldman (@svovaf)
25754 * @since 2.0.0
25755 *
25756 * @return string
25757 */
25758 function fetch_remote_icon_url() {
25759 $info = $this->fetch_module_info();
25760
25761 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25762 $info->icon :
25763 '';
25764 }
25765
25766 #--------------------------------------------------------------------------------
25767 #region GDPR
25768 #--------------------------------------------------------------------------------
25769
25770 /**
25771 * @author Leo Fajardo (@leorw)
25772 * @since 2.1.0
25773 *
25774 * @param array $user_plugins
25775 *
25776 * @return string
25777 */
25778 private function get_gdpr_admin_notice_string( $user_plugins ) {
25779 $this->_logger->entrance();
25780
25781 $addons = self::get_all_addons();
25782
25783 foreach ( $user_plugins as $user_plugin ) {
25784 $has_addons = isset( $addons[ $user_plugin->id ] );
25785
25786 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25787 if ( $this->_module_id == $user_plugin->id ) {
25788 $addons = $this->get_addons();
25789 $has_addons = ( ! empty( $addons ) );
25790 } else {
25791 $plugin_api = FS_Api::instance(
25792 $user_plugin->id,
25793 'plugin',
25794 $user_plugin->id,
25795 $user_plugin->public_key,
25796 ! $user_plugin->is_live,
25797 false,
25798 $this->get_sdk_version()
25799 );
25800
25801 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25802
25803 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25804 is_array( $addons_result->plugins ) &&
25805 ! empty( $addons_result->plugins )
25806 ) {
25807 $has_addons = true;
25808 }
25809 }
25810 }
25811
25812 $user_plugin->has_addons = $has_addons;
25813 }
25814
25815 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25816
25817 $multiple_products_text = '';
25818
25819 if ( $is_single_parent_product ) {
25820 $single_parent_product = reset( $user_plugins );
25821
25822 $thank_you = sprintf(
25823 "<span data-plugin-id='%d'>%s</span>",
25824 $single_parent_product->id,
25825 sprintf(
25826 $single_parent_product->has_addons ?
25827 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25828 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25829 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25830 )
25831 );
25832
25833 $already_opted_in = sprintf(
25834 $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' ),
25835 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25836 );
25837 } else {
25838 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25839 $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' );
25840
25841 $products_and_add_ons = '';
25842 foreach ( $user_plugins as $user_plugin ) {
25843 if ( ! empty( $products_and_add_ons ) ) {
25844 $products_and_add_ons .= ', ';
25845 }
25846
25847 if ( ! $user_plugin->has_addons ) {
25848 $products_and_add_ons .= sprintf(
25849 "<span data-plugin-id='%d'>%s</span>",
25850 $user_plugin->id,
25851 $user_plugin->title
25852 );
25853 } else {
25854 $products_and_add_ons .= sprintf(
25855 "<span data-plugin-id='%d'>%s</span>",
25856 $user_plugin->id,
25857 sprintf(
25858 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25859 $user_plugin->title
25860 )
25861 );
25862 }
25863 }
25864
25865 $multiple_products_text = sprintf(
25866 "<small class='products'><strong>%s:</strong> %s</small>",
25867 $this->get_text_inline( 'Products', 'products' ),
25868 $products_and_add_ons
25869 );
25870 }
25871
25872 $actions = sprintf(
25873 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25874 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25875 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25876 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25877 sprintf(
25878 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25879 '<span class="underlined">',
25880 '</span>'
25881 )
25882 );
25883
25884 return sprintf(
25885 '%s %s %s',
25886 $thank_you,
25887 $already_opted_in,
25888 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://eugdpr.org/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
25889 '<br><br>' .
25890 '<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>' .
25891 $actions .
25892 ( $is_single_parent_product ? '' : $multiple_products_text )
25893 );
25894 }
25895
25896 /**
25897 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25898 * plugins and themes they've opted in to.
25899 *
25900 * @author Leo Fajardo (@leorw)
25901 * @since 2.1.0
25902 *
25903 * @param string $user_email
25904 * @param string $license_key
25905 * @param array $plugin_ids
25906 * @param string|null $license_key
25907 *
25908 * @return array|false
25909 */
25910 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25911 $request = array(
25912 'method' => 'POST',
25913 'body' => array(),
25914 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25915 );
25916
25917 if ( is_string( $user_email ) ) {
25918 $request['body']['email'] = $user_email;
25919 } else {
25920 $request['body']['license_key'] = $license_key;
25921 }
25922
25923 $result = array();
25924
25925 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25926 $total_plugin_ids = count( $plugin_ids );
25927
25928 $plugin_ids_count_per_request = 10;
25929 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25930 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25931
25932 $request['body']['plugin_ids'] = $plugin_ids_set;
25933
25934 $response = self::safe_remote_post(
25935 $url,
25936 $request,
25937 WP_FS__TIME_24_HOURS_IN_SEC,
25938 WP_FS__TIME_12_HOURS_IN_SEC
25939 );
25940
25941 if ( ! is_wp_error( $response ) ) {
25942 $decoded = is_string( $response['body'] ) ?
25943 json_decode( $response['body'] ) :
25944 null;
25945
25946 if (
25947 !is_object($decoded) ||
25948 !isset($decoded->success) ||
25949 true !== $decoded->success ||
25950 !isset( $decoded->data ) ||
25951 !is_array( $decoded->data )
25952 ) {
25953 return false;
25954 }
25955
25956 $result = array_merge( $result, $decoded->data );
25957 }
25958 }
25959
25960 return $result;
25961 }
25962
25963 /**
25964 * @author Leo Fajardo (@leorw)
25965 * @since 2.1.0
25966 */
25967 function _maybe_show_gdpr_admin_notice() {
25968 if ( ! $this->is_user_in_admin() ) {
25969 return;
25970 }
25971
25972 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25973 return;
25974 }
25975
25976 if ( ! $this->is_user_admin() ) {
25977 return;
25978 }
25979
25980 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25981
25982 $lock = FS_User_Lock::instance();
25983
25984 /**
25985 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25986 */
25987 if ( ! $lock->try_lock( 60 ) ) {
25988 return;
25989 }
25990
25991 /**
25992 * @var $current_wp_user WP_User
25993 */
25994 $current_wp_user = self::_get_current_wp_user();
25995
25996 /**
25997 * @var FS_User $current_fs_user
25998 */
25999 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
26000
26001 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
26002
26003 if ( ! is_object( $current_fs_user ) ) {
26004 // 10-year lock.
26005 $lock->lock( $ten_years_in_sec );
26006
26007 return;
26008 }
26009
26010 $gdpr = FS_GDPR_Manager::instance();
26011
26012 if ( $gdpr->is_opt_in_notice_shown() ) {
26013 // 30-day lock.
26014 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26015
26016 return;
26017 }
26018
26019 if ( ! $gdpr->should_show_opt_in_notice() ) {
26020 // 10-year lock.
26021 $lock->lock( $ten_years_in_sec );
26022
26023 return;
26024 }
26025
26026 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
26027 $was_notice_shown_before = ( false !== $last_time_notice_shown );
26028
26029 if ( $was_notice_shown_before &&
26030 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26031 ) {
26032 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26033 return;
26034 }
26035
26036 /**
26037 * Find all plugin IDs that were installed by the current admin.
26038 */
26039 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26040
26041 if ( empty( $plugin_ids_map )) {
26042 $lock->lock( $ten_years_in_sec );
26043
26044 return;
26045 }
26046
26047 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26048 $current_fs_user->email,
26049 null,
26050 array_keys( $plugin_ids_map )
26051 );
26052
26053 if ( empty( $user_plugins ) ) {
26054 $lock->lock(
26055 is_array($user_plugins) ?
26056 $ten_years_in_sec :
26057 // Lock for 24-hours on errors.
26058 WP_FS__TIME_24_HOURS_IN_SEC
26059 );
26060
26061 return;
26062 }
26063
26064 $has_unset_marketing_optin = false;
26065
26066 foreach ( $user_plugins as $user_plugin ) {
26067 if ( true == $user_plugin->is_marketing_allowed ) {
26068 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26069 }
26070
26071 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26072 $has_unset_marketing_optin = true;
26073 }
26074 }
26075
26076 if ( empty( $plugin_ids_map ) ||
26077 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26078 ) {
26079 $lock->lock( $ten_years_in_sec );
26080
26081 return;
26082 }
26083
26084 $modules = array_merge(
26085 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26086 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26087 );
26088
26089 foreach ( $modules as $module ) {
26090 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26091 $plugin_ids_map[ $module->id ] = $module;
26092 }
26093 }
26094
26095 $plugin_title = null;
26096 if ( 1 === count( $plugin_ids_map ) ) {
26097 $module = reset( $plugin_ids_map );
26098 $plugin_title = $module->title;
26099 }
26100
26101 $gdpr->add_opt_in_sticky_notice(
26102 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26103 $plugin_title
26104 );
26105
26106 $this->add_gdpr_optin_ajax_handler_and_style();
26107
26108 $gdpr->notice_was_just_shown();
26109
26110 // 30-day lock.
26111 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26112 }
26113
26114 /**
26115 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26116 * marketing.
26117 *
26118 * @author Leo Fajardo (@leorw)
26119 * @since 2.1.0
26120 */
26121 private function disable_opt_in_notice_and_lock_user() {
26122 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26123
26124 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26125
26126 // 10-year lock.
26127 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26128 }
26129
26130 /**
26131 * @author Leo Fajardo (@leorw)
26132 * @since 2.5.4
26133 */
26134 static function _add_api_connectivity_notice_handler_js() {
26135 fs_require_once_template( 'api-connectivity-message-js.php' );
26136 }
26137
26138 /**
26139 * @author Leo Fajardo (@leorw)
26140 * @since 2.1.0
26141 */
26142 function _add_gdpr_optin_js() {
26143 $vars = array( 'id' => $this->_module_id );
26144
26145 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26146 }
26147
26148 /**
26149 * @author Leo Fajardo (@leorw)
26150 * @since 2.1.0
26151 */
26152 function enqueue_gdpr_optin_notice_style() {
26153 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26154 }
26155
26156 /**
26157 * @author Leo Fajardo (@leorw)
26158 * @since 2.1.0
26159 */
26160 function _maybe_add_gdpr_optin_ajax_handler() {
26161 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26162
26163 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26164 $this->add_gdpr_optin_ajax_handler_and_style();
26165 }
26166 }
26167
26168 /**
26169 * @author Leo Fajardo (@leorw)
26170 * @since 2.1.0
26171 */
26172 function _fetch_is_marketing_required_flag_value_ajax_action() {
26173 $this->_logger->entrance();
26174
26175 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26176
26177 $license_key = fs_request_get( 'license_key' );
26178
26179 if ( empty($license_key) ) {
26180 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26181 }
26182
26183 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26184 null,
26185 $license_key,
26186 array( $this->_module_id )
26187 );
26188
26189 if ( ! is_array( $user_plugins ) ||
26190 empty($user_plugins) ||
26191 !isset($user_plugins[0]->plugin_id) ||
26192 $user_plugins[0]->plugin_id != $this->_module_id
26193 ) {
26194 /**
26195 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26196 *
26197 * @author Vova Feldman (@svovaf)
26198 */
26199 self::shoot_ajax_success( array(
26200 'is_marketing_allowed' => null,
26201 'license_owner_id' => null
26202 ) );
26203 }
26204
26205 self::shoot_ajax_success( array(
26206 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26207 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26208 ) );
26209 }
26210
26211 /**
26212 * @author Leo Fajardo (@leorw)
26213 * @since 2.3.2
26214 *
26215 * @param number[] $install_ids
26216 *
26217 * @return array {
26218 * An array of objects containing the installs' licenses owners data.
26219 *
26220 * @property number $id User ID.
26221 * @property string $email User email (can be masked email).
26222 * }
26223 */
26224 private function fetch_installs_licenses_owners_data( $install_ids ) {
26225 $this->_logger->entrance();
26226
26227 $response = $this->get_api_user_scope()->get(
26228 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26229 );
26230
26231 $license_owners = array();
26232
26233 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26234 $license_owners = $response->owners;
26235 }
26236
26237 return $license_owners;
26238 }
26239
26240 /**
26241 * @author Leo Fajardo (@leorw)
26242 * @since 2.1.0
26243 */
26244 private function add_gdpr_optin_ajax_handler_and_style() {
26245 // Add GDPR action AJAX callback.
26246 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26247
26248 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26249 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26250 }
26251
26252 /**
26253 * @author Leo Fajardo (@leorw)
26254 * @since 2.1.0
26255 */
26256 function _gdpr_optin_ajax_action() {
26257 $this->_logger->entrance();
26258
26259 $this->check_ajax_referer( 'gdpr_optin_action' );
26260
26261 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26262 self::shoot_ajax_failure();
26263 }
26264
26265 $current_wp_user = self::_get_current_wp_user();
26266
26267 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26268 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26269 self::shoot_ajax_failure();
26270 }
26271
26272 $modules = array_merge(
26273 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26274 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26275 );
26276
26277 foreach ( $modules as $key => $module ) {
26278 if ( ! in_array( $module->id, $plugin_ids ) ) {
26279 unset( $modules[ $key ] );
26280 }
26281 }
26282
26283 if ( empty( $modules ) ) {
26284 self::shoot_ajax_failure();
26285 }
26286
26287 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26288
26289 foreach ( $modules as $module ) {
26290 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26291 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26292 ) );
26293 }
26294
26295 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26296
26297 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26298
26299 // 10-year lock.
26300 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26301
26302 self::shoot_ajax_success();
26303 }
26304
26305 /**
26306 * 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.
26307 *
26308 * @author Vova Feldman (@svovaf)
26309 * @since 2.1.0
26310 *
26311 * @return bool
26312 */
26313 private function should_handle_gdpr_admin_notice() {
26314 return $this->apply_filters(
26315 'handle_gdpr_admin_notice',
26316 // Default to false.
26317 false
26318 );
26319 }
26320
26321 #endregion
26322
26323 #----------------------------------------------------------------------------------
26324 #region Marketing
26325 #----------------------------------------------------------------------------------
26326
26327 /**
26328 * Check if current user purchased any other plugins before.
26329 *
26330 * @author Vova Feldman (@svovaf)
26331 * @since 1.0.9
26332 *
26333 * @return bool
26334 */
26335 function has_purchased_before() {
26336 // TODO: Implement has_purchased_before() method.
26337 throw new Exception( 'not implemented' );
26338 }
26339
26340 /**
26341 * Check if current user classified as an agency.
26342 *
26343 * @author Vova Feldman (@svovaf)
26344 * @since 1.0.9
26345 *
26346 * @return bool
26347 */
26348 function is_agency() {
26349 // TODO: Implement is_agency() method.
26350 throw new Exception( 'not implemented' );
26351 }
26352
26353 /**
26354 * Check if current user classified as a developer.
26355 *
26356 * @author Vova Feldman (@svovaf)
26357 * @since 1.0.9
26358 *
26359 * @return bool
26360 */
26361 function is_developer() {
26362 // TODO: Implement is_developer() method.
26363 throw new Exception( 'not implemented' );
26364 }
26365
26366 /**
26367 * Check if current user classified as a business.
26368 *
26369 * @author Vova Feldman (@svovaf)
26370 * @since 1.0.9
26371 *
26372 * @return bool
26373 */
26374 function is_business() {
26375 // TODO: Implement is_business() method.
26376 throw new Exception( 'not implemented' );
26377 }
26378
26379 #endregion
26380
26381 #----------------------------------------------------------------------------------
26382 #region Helper
26383 #----------------------------------------------------------------------------------
26384
26385 /**
26386 * If running with a secret key, assume it's the developer and show pending plans as well.
26387 *
26388 * @author Vova Feldman (@svovaf)
26389 * @since 2.1.2
26390 *
26391 * @param string $path
26392 *
26393 * @return string
26394 */
26395 function add_show_pending( $path ) {
26396 if ( ! $this->has_secret_key() ) {
26397 return $path;
26398 }
26399
26400 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26401 }
26402
26403 #endregion
26404 }
26405