PluginProbe
TablePress – Tables in WordPress made easy / 2.1.8
TablePress – Tables in WordPress made easy v2.1.8
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / libraries / freemius / includes / class-freemius.php

class-freemius.php in TablePress – Tables in WordPress made easy 2.1.8, at libraries/freemius/includes/class-freemius.php

26,387 lines 739.4 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' ) ); // @phpstan-ignore-line
1535 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
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_raw( '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 void
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 sdk@freemius.com 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 $url = ( ! is_multisite() && defined( 'WP_SITEURL' ) ) ? WP_SITEURL : self::get_site_url_from_wp_option( $blog_id );
3496
3497 if ( $strip_protocol ) {
3498 $url = fs_strip_url_protocol( $url );
3499 }
3500
3501 if ( $add_trailing_slash ) {
3502 $url = trailingslashit( $url );
3503 }
3504
3505 return $url;
3506 }
3507
3508 /**
3509 * @author Leo Fajardo (@leorw)
3510 * @since 2.6.0
3511 *
3512 * @param int|null $blog_id
3513 *
3514 * @return string
3515 */
3516 private static function get_site_url_from_wp_option( $blog_id = null ) {
3517 global $wp_filter;
3518
3519 $site_url_filters = array(
3520 'site_url' => null,
3521 'pre_option_siteurl' => null,
3522 'default_option_siteurl' => null,
3523 'option_siteurl' => null,
3524 );
3525
3526 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3527 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3528 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3529 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3530 unset( $wp_filter[ $hook_name ] );
3531 }
3532 }
3533
3534 $url = get_site_url( $blog_id );
3535
3536 // Re-attach the filters back.
3537 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3538 if ( ! empty( $site_url_filter ) ) {
3539 $wp_filter[ $hook_name ] = $site_url_filter;
3540 }
3541 }
3542
3543 return $url;
3544 }
3545
3546 /**
3547 * @author Leo Fajardo (@leorw)
3548 * @since 2.5.0
3549 *
3550 * @param number $site_id
3551 */
3552 function fetch_install_by_id( $site_id ) {
3553 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3554 }
3555
3556 /**
3557 * @author Leo Fajardo (@leorw)
3558 * @since 2.5.0
3559 *
3560 * @return string|object|bool
3561 */
3562 function _handle_long_term_duplicate() {
3563 $this->_logger->entrance();
3564
3565 $this->delete_current_install( false );
3566
3567 $license_key = false;
3568
3569 if (
3570 is_object( $this->_license ) &&
3571 ! $this->_license->is_utilized(
3572 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3573 )
3574 ) {
3575 $license_key = $this->_license->secret_key;
3576 }
3577
3578 return $this->opt_in(
3579 false,
3580 false,
3581 false,
3582 $license_key,
3583 false,
3584 false,
3585 false,
3586 null,
3587 array(),
3588 false
3589 );
3590 }
3591
3592 #endregion
3593
3594 /**
3595 * @author Leo Fajardo (@leorw)
3596 *
3597 * @since 2.1.3
3598 */
3599 private static function migrate_options_to_network() {
3600 self::migrate_accounts_to_network();
3601
3602 // Migrate API options from site level to network level.
3603 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3604 $api_network_options->migrate_to_network();
3605
3606 // Migrate API cache to network level storage.
3607 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3608
3609 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3610 }
3611
3612 #----------------------------------------------------------------------------------
3613 #region Localization
3614 #----------------------------------------------------------------------------------
3615
3616 /**
3617 * Load framework's text domain.
3618 *
3619 * @author Vova Feldman (@svovaf)
3620 * @since 1.2.1
3621 */
3622 static function _load_textdomain() {
3623 if ( ! is_admin() ) {
3624 return;
3625 }
3626
3627 global $fs_active_plugins;
3628
3629 // Works both for plugins and themes.
3630 load_plugin_textdomain(
3631 'freemius',
3632 false,
3633 $fs_active_plugins->newest->sdk_path . '/languages/'
3634 );
3635 }
3636
3637 #endregion
3638
3639 #----------------------------------------------------------------------------------
3640 #region Debugging
3641 #----------------------------------------------------------------------------------
3642
3643 /**
3644 * @author Vova Feldman (@svovaf)
3645 * @since 1.0.8
3646 */
3647 static function _add_debug_section() {
3648 if ( ! is_super_admin() ) {
3649 // Add debug page only for super-admins.
3650 return;
3651 }
3652
3653 self::$_static_logger->entrance();
3654
3655 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3656
3657 if ( WP_FS__DEV_MODE ) {
3658 // Add top-level debug menu item.
3659 $hook = FS_Admin_Menu_Manager::add_page(
3660 $title,
3661 $title,
3662 'manage_options',
3663 'freemius',
3664 array( 'Freemius', '_debug_page_render' )
3665 );
3666 } else {
3667 // Add hidden debug page.
3668 $hook = FS_Admin_Menu_Manager::add_subpage(
3669 '',
3670 $title,
3671 $title,
3672 'manage_options',
3673 'freemius',
3674 array( 'Freemius', '_debug_page_render' )
3675 );
3676 }
3677
3678 if ( ! empty( $hook ) ) {
3679 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3680 }
3681 }
3682
3683 /**
3684 * @author Vova Feldman (@svovaf)
3685 * @since 1.1.7.3
3686 */
3687 static function _toggle_debug_mode() {
3688 check_admin_referer( 'fs_toggle_debug_mode' );
3689
3690 if ( ! is_super_admin() ) {
3691 return;
3692 }
3693
3694 $is_on = fs_request_get( 'is_on', false, 'post' );
3695
3696 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3697 update_option( 'fs_debug_mode', $is_on );
3698
3699 // Turn on/off storage logging.
3700 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3701 }
3702
3703 exit;
3704 }
3705
3706 /**
3707 * @author Vova Feldman (@svovaf)
3708 * @since 1.2.1.6
3709 */
3710 static function _get_debug_log() {
3711 check_admin_referer( 'fs_get_debug_log' );
3712
3713 if ( ! is_super_admin() ) {
3714 return;
3715 }
3716
3717 $limit = min( ! empty( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 200, 200 );
3718 $offset = min( ! empty( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 200, 200 );
3719
3720 $logs = FS_Logger::load_db_logs(
3721 fs_request_get( 'filters', false, 'post' ),
3722 $limit,
3723 $offset
3724 );
3725
3726 self::shoot_ajax_success( $logs );
3727 }
3728
3729 /**
3730 * @author Vova Feldman (@svovaf)
3731 * @since 1.2.1.7
3732 */
3733 static function _get_db_option() {
3734 check_admin_referer( 'fs_get_db_option' );
3735
3736 $option_name = fs_request_get( 'option_name' );
3737
3738 if ( ! is_super_admin() ||
3739 ! fs_starts_with( $option_name, 'fs_' )
3740 ) {
3741 self::shoot_ajax_failure();
3742 }
3743
3744 $value = get_option( $option_name );
3745
3746 $result = array(
3747 'name' => $option_name,
3748 );
3749
3750 if ( false !== $value ) {
3751 if ( ! is_string( $value ) ) {
3752 $value = json_encode( $value );
3753 }
3754
3755 $result['value'] = $value;
3756 }
3757
3758 self::shoot_ajax_success( $result );
3759 }
3760
3761 /**
3762 * @author Vova Feldman (@svovaf)
3763 * @since 1.2.1.7
3764 */
3765 static function _set_db_option() {
3766 check_admin_referer( 'fs_set_db_option' );
3767
3768 $option_name = fs_request_get( 'option_name' );
3769
3770 if ( ! is_super_admin() ||
3771 ! fs_starts_with( $option_name, 'fs_' )
3772 ) {
3773 self::shoot_ajax_failure();
3774 }
3775
3776 $option_value = fs_request_get_raw( 'option_value' );
3777
3778 if ( ! empty( $option_value ) ) {
3779 update_option( $option_name, $option_value );
3780 }
3781
3782 self::shoot_ajax_success();
3783 }
3784
3785 /**
3786 * @author Vova Feldman (@svovaf)
3787 * @since 1.0.8
3788 */
3789 static function _debug_page_actions() {
3790 self::_clean_admin_content_section();
3791
3792 if ( fs_request_is_action( 'restart_freemius' ) ) {
3793 check_admin_referer( 'restart_freemius' );
3794
3795 if ( ! is_multisite() ) {
3796 // Clear accounts data.
3797 self::$_accounts->clear( null, true );
3798 } else {
3799 $sites = self::get_sites();
3800 foreach ( $sites as $site ) {
3801 $blog_id = self::get_site_blog_id( $site );
3802 self::$_accounts->clear( $blog_id, true );
3803 }
3804
3805 // Clear network level storage.
3806 self::$_accounts->clear( true, true );
3807 }
3808
3809 // Clear SDK reference cache.
3810 delete_option( 'fs_active_plugins' );
3811 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3812 check_admin_referer( 'clear_updates_data' );
3813
3814 if ( ! is_multisite() ) {
3815 set_site_transient( 'update_plugins', null );
3816 set_site_transient( 'update_themes', null );
3817 } else {
3818 $current_blog_id = get_current_blog_id();
3819
3820 $sites = self::get_sites();
3821 foreach ( $sites as $site ) {
3822 switch_to_blog( self::get_site_blog_id( $site ) );
3823
3824 set_site_transient( 'update_plugins', null );
3825 set_site_transient( 'update_themes', null );
3826 }
3827
3828 switch_to_blog( $current_blog_id );
3829 }
3830 } else if ( fs_request_is_action( 'reset_deactivation_snoozing' ) ) {
3831 check_admin_referer( 'reset_deactivation_snoozing' );
3832
3833 self::reset_deactivation_snoozing();
3834 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3835 check_admin_referer( 'simulate_trial' );
3836
3837 $fs = freemius( fs_request_get( 'module_id' ) );
3838
3839 // Update SDK install to at least 24 hours before.
3840 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3841 // Unset the trial shown timestamp.
3842 unset( $fs->_storage->trial_promotion_shown );
3843 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3844 check_admin_referer( 'simulate_network_upgrade' );
3845
3846 $fs = freemius( fs_request_get( 'module_id' ) );
3847
3848 self::set_network_upgrade_mode( $fs->_storage );
3849 } else if ( fs_request_is_action( 'delete_install' ) ) {
3850 check_admin_referer( 'delete_install' );
3851
3852 self::_delete_site_by_slug(
3853 fs_request_get( 'slug' ),
3854 fs_request_get( 'module_type' ),
3855 true,
3856 fs_request_get( 'blog_id', null )
3857 );
3858 } else if ( fs_request_is_action( 'delete_user' ) ) {
3859 check_admin_referer( 'delete_user' );
3860
3861 self::delete_user( fs_request_get( 'user_id' ) );
3862 } else if ( fs_request_is_action( 'download_logs' ) ) {
3863 check_admin_referer( 'download_logs' );
3864
3865 $download_url = FS_Logger::download_db_logs(
3866 fs_request_get( 'filters', false, 'post' )
3867 );
3868
3869 if ( false === $download_url ) {
3870 wp_die( 'Oops... there was an error while generating the logs download file. Please try again and if it doesn\'t work contact support@freemius.com.' );
3871 }
3872
3873 fs_redirect( $download_url );
3874 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3875 check_admin_referer( 'migrate_options_to_network' );
3876
3877 self::migrate_options_to_network();
3878 }
3879 }
3880
3881 /**
3882 * @author Leo Fajardo (@leorw)
3883 * @since 2.5.0
3884 *
3885 * @return array
3886 */
3887 static function get_all_modules_sites() {
3888 self::$_static_logger->entrance();
3889
3890 $sites_by_type = array(
3891 WP_FS__MODULE_TYPE_PLUGIN => array(),
3892 WP_FS__MODULE_TYPE_THEME => array(),
3893 );
3894
3895 $module_types = array_keys( $sites_by_type );
3896
3897 if ( ! is_multisite() ) {
3898 foreach ( $module_types as $type ) {
3899 $sites_by_type[ $type ] = self::get_all_sites( $type );
3900
3901 foreach ( $sites_by_type[ $type ] as $slug => $install ) {
3902 $sites_by_type[ $type ][ $slug ] = array( $install );
3903 }
3904 }
3905 } else {
3906 $sites = self::get_sites();
3907
3908 foreach ( $sites as $site ) {
3909 $blog_id = self::get_site_blog_id( $site );
3910
3911 foreach ( $module_types as $type ) {
3912 $installs = self::get_all_sites( $type, $blog_id );
3913
3914 foreach ( $installs as $slug => $install ) {
3915 if ( ! isset( $sites_by_type[ $type ][ $slug ] ) ) {
3916 $sites_by_type[ $type ][ $slug ] = array();
3917 }
3918
3919 $install->blog_id = $blog_id;
3920
3921 $sites_by_type[ $type ][ $slug ][] = $install;
3922 }
3923
3924 }
3925 }
3926 }
3927
3928 return $sites_by_type;
3929 }
3930
3931 /**
3932 * @author Vova Feldman (@svovaf)
3933 * @since 1.0.8
3934 */
3935 static function _debug_page_render() {
3936 self::$_static_logger->entrance();
3937
3938 $all_modules_sites = self::get_all_modules_sites();
3939
3940 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3941
3942 $vars = array(
3943 'plugin_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_PLUGIN ],
3944 'theme_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_THEME ],
3945 'users' => self::get_all_users(),
3946 'addons' => self::get_all_addons(),
3947 'account_addons' => self::get_all_account_addons(),
3948 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3949 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3950 );
3951
3952 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3953 fs_require_once_template( 'debug.php', $vars );
3954 }
3955
3956 #endregion
3957
3958 #----------------------------------------------------------------------------------
3959 #region Connectivity Issues
3960 #----------------------------------------------------------------------------------
3961
3962 /**
3963 * Check if Freemius should be turned on for the current plugin install.
3964 *
3965 * Note:
3966 * $this->_is_on is updated in has_api_connectivity()
3967 *
3968 * @author Vova Feldman (@svovaf)
3969 * @since 1.0.9
3970 *
3971 * @return bool
3972 */
3973 function is_on() {
3974 self::$_static_logger->entrance();
3975
3976 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
3977 return false;
3978 }
3979
3980 if ( isset( $this->_is_on ) ) {
3981 return $this->_is_on;
3982 }
3983
3984 // If already installed or pending then sure it's on :)
3985 if ( $this->is_registered() || $this->is_pending_activation() ) {
3986 $this->_is_on = true;
3987
3988 return true;
3989 }
3990
3991 return false;
3992 }
3993
3994 /**
3995 * @author Vova Feldman (@svovaf)
3996 * @since 1.1.7.3
3997 *
3998 * @param bool $flush_if_no_connectivity
3999 *
4000 * @return bool
4001 */
4002 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
4003 if ( ! isset( $this->_storage->connectivity_test ) ) {
4004 // Connectivity test was never executed, or cache was cleared.
4005 return true;
4006 }
4007
4008 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
4009 if ( WP_FS__IS_HTTP_REQUEST ) {
4010 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
4011 // Domain changed.
4012 return true;
4013 }
4014
4015 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
4016 // Server IP changed.
4017 return true;
4018 }
4019 }
4020 }
4021
4022 if ( $this->_storage->connectivity_test['is_connected'] &&
4023 $this->_storage->connectivity_test['is_active']
4024 ) {
4025 // API connected and Freemius is active - no need to run connectivity check.
4026 return false;
4027 }
4028
4029 if ( $flush_if_no_connectivity ) {
4030 /**
4031 * If explicitly asked to flush when no connectivity - do it only
4032 * if at least 10 sec passed from the last API connectivity test.
4033 */
4034 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
4035 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
4036 }
4037
4038 /**
4039 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
4040 */
4041 $version = $this->get_plugin_version();
4042 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
4043 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
4044 return true;
4045 }
4046
4047 return false;
4048 }
4049
4050 /**
4051 * @author Leo Fajardo (@leorw)
4052 * @since 2.5.4
4053 *
4054 * @param bool $is_update
4055 *
4056 * @return bool
4057 */
4058 private function should_turn_fs_on( $is_update = true ) {
4059 if (
4060 empty( $this->_plugin->opt_in_moderation ) ||
4061 ! is_array( $this->_plugin->opt_in_moderation )
4062 ) {
4063 return true;
4064 }
4065
4066 $optin_config = $this->_plugin->opt_in_moderation;
4067
4068 if (
4069 WP_FS__IS_LOCALHOST &&
4070 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
4071 ) {
4072 return true;
4073 }
4074
4075 $optin_config_key = $is_update ?
4076 'updates' :
4077 'new';
4078
4079 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
4080 return true;
4081 }
4082
4083 $visibility_percentage = $optin_config[ $optin_config_key ];
4084
4085 if ( 0 == $visibility_percentage ) {
4086 return false;
4087 }
4088
4089 if ( ! is_numeric( $visibility_percentage ) ) {
4090 return true;
4091 }
4092
4093 $min = 1;
4094 $max = 100;
4095
4096 if ( function_exists( 'random_int' ) ) {
4097 $random = random_int( $min, $max ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound
4098 } else {
4099 $random = rand( $min, $max );
4100 }
4101
4102 return ( $random <= $visibility_percentage );
4103 }
4104
4105 /**
4106 * Check if there's any connectivity issue to Freemius API.
4107 *
4108 * @author Vova Feldman (@svovaf)
4109 * @since 1.0.9
4110 *
4111 * @param bool $flush_if_no_connectivity
4112 *
4113 * @return bool|null
4114 */
4115 function has_api_connectivity( $flush_if_no_connectivity = false ) {
4116 $this->_logger->entrance();
4117
4118 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
4119 return $this->_has_api_connection;
4120 }
4121
4122 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
4123 isset( $this->_storage->connectivity_test ) &&
4124 true === $this->_storage->connectivity_test['is_connected']
4125 ) {
4126 $this->clear_connectivity_info();
4127 }
4128
4129 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
4130 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
4131 /**
4132 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
4133 *
4134 * @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.
4135 */
4136 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
4137 $this->is_premium() ||
4138 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4139
4140 return $this->_has_api_connection;
4141 }
4142
4143 if (
4144 ! empty( $this->_storage->connectivity_test ) &&
4145 isset( $this->_storage->connectivity_test['is_active'] )
4146 ) {
4147 $is_active = $this->_storage->connectivity_test['is_active'];
4148 } else {
4149 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
4150
4151 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), null );
4152 }
4153
4154 if ( $is_active ) {
4155 $this->_is_on = true;
4156 }
4157
4158 return $this->_has_api_connection;
4159 }
4160
4161 /**
4162 * @author Leo Fajardo (@leorw)
4163 * @since 2.5.4
4164 */
4165 private function clear_connectivity_info() {
4166 unset( $this->_storage->connectivity_test );
4167
4168 FS_Api::clear_force_http_flag();
4169 }
4170
4171 /**
4172 * @author Vova Feldman (@svovaf)
4173 * @since 1.1.7.4
4174 *
4175 * @param object $pong
4176 * @param bool|null $is_connected
4177 */
4178 private function store_connectivity_info( $pong, $is_connected ) {
4179 $this->_logger->entrance();
4180
4181 $version = $this->get_plugin_version();
4182
4183 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
4184 $is_active = false;
4185 } else {
4186 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
4187 }
4188
4189 $is_active = $this->apply_filters(
4190 'is_on',
4191 $is_active,
4192 $this->is_plugin_update(),
4193 $version
4194 );
4195
4196 $this->_storage->connectivity_test = array(
4197 'is_connected' => $is_connected,
4198 'host' => $_SERVER['HTTP_HOST'],
4199 'server_ip' => WP_FS__REMOTE_ADDR,
4200 'is_active' => $is_active,
4201 'timestamp' => WP_FS__SCRIPT_START_TIME,
4202 // Last version with connectivity attempt.
4203 'version' => $version,
4204 );
4205
4206 $this->_has_api_connection = $is_connected;
4207 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4208 }
4209
4210 /**
4211 * @author Leo Fajardo (@leorw)
4212 * @since 2.5.4
4213 *
4214 * @param bool $is_connected
4215 */
4216 private function update_connectivity_info( $is_connected ) {
4217 $this->store_connectivity_info(
4218 // 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.
4219 (object) array( 'is_active' => true ),
4220 $is_connected
4221 );
4222 }
4223
4224 /**
4225 * Force turning Freemius on.
4226 *
4227 * @author Vova Feldman (@svovaf)
4228 * @since 1.1.8.1
4229 *
4230 * @return bool TRUE if successfully turned on.
4231 */
4232 private function turn_on() {
4233 $this->_logger->entrance();
4234
4235 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4236 return false;
4237 }
4238
4239 $updated_connectivity = $this->_storage->connectivity_test;
4240 $updated_connectivity['is_active'] = true;
4241 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4242 $this->_storage->connectivity_test = $updated_connectivity;
4243
4244 $this->_is_on = true;
4245
4246 return true;
4247 }
4248
4249 /**
4250 * Anonymous and unique site identifier (Hash).
4251 *
4252 * @author Vova Feldman (@svovaf)
4253 * @since 1.1.0
4254 *
4255 * @param null|int $blog_id Since 2.0.0
4256 *
4257 * @return string
4258 */
4259 function get_anonymous_id( $blog_id = null ) {
4260 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4261
4262 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4263 $key = self::get_unfiltered_site_url( $blog_id, true );
4264
4265 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4266 if ( empty( $secure_auth ) ||
4267 false !== strpos( $secure_auth, ' ' ) ||
4268 'put your unique phrase here' === $secure_auth
4269 ) {
4270 // Protect against default auth key.
4271 $secure_auth = md5( microtime() );
4272 }
4273
4274 /**
4275 * Base the unique identifier on the WP secure authentication key. Which
4276 * turns the key into a secret anonymous identifier. This will help us
4277 * to avoid duplicate installs generation on the backend upon opt-in.
4278 *
4279 * @author Vova Feldman (@svovaf)
4280 * @since 1.2.3
4281 */
4282 $unique_id = md5( $key . $secure_auth );
4283
4284 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4285 }
4286
4287 $this->_logger->departure( $unique_id );
4288
4289 return $unique_id;
4290 }
4291
4292 /**
4293 * Returns anonymous network ID.
4294 *
4295 * @since 2.4.3
4296 *
4297 * @return string
4298 */
4299 function get_anonymous_network_id() {
4300 return $this->get_anonymous_id( get_network()->site_id );
4301 }
4302
4303 /**
4304 * @author Vova Feldman (@svovaf)
4305 * @since 1.1.7.4
4306 *
4307 * @return \WP_User
4308 */
4309 static function _get_current_wp_user() {
4310 self::require_pluggable_essentials();
4311 self::wp_cookie_constants();
4312
4313 return wp_get_current_user();
4314 }
4315
4316 /**
4317 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4318 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4319 * is network activated the cookie constants are only configured after the network
4320 * plugins activation, therefore, if we don't define those constants WP will throw
4321 * PHP warnings/notices.
4322 *
4323 * @author Vova Feldman (@svovaf)
4324 * @since 2.1.1
4325 */
4326 private static function wp_cookie_constants() {
4327 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4328 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4329 ) {
4330 return;
4331 }
4332
4333 /**
4334 * Used to guarantee unique hash cookies
4335 *
4336 * @since 1.5.0
4337 */
4338 if ( ! defined( 'COOKIEHASH' ) ) {
4339 $siteurl = get_site_option( 'siteurl' );
4340 if ( $siteurl ) {
4341 define( 'COOKIEHASH', md5( $siteurl ) );
4342 } else {
4343 define( 'COOKIEHASH', '' );
4344 }
4345 }
4346
4347 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4348 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4349 }
4350
4351 /**
4352 * @since 2.5.0
4353 */
4354 if ( ! defined( 'AUTH_COOKIE' ) ) {
4355 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4356 }
4357
4358 /**
4359 * @since 2.6.0
4360 */
4361 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4362 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4363 }
4364 }
4365
4366 /**
4367 * @author Vova Feldman (@svovaf)
4368 * @since 2.1.0
4369 *
4370 * @return int
4371 */
4372 static function get_current_wp_user_id() {
4373 $wp_user = self::_get_current_wp_user();
4374
4375 return $wp_user->ID;
4376 }
4377
4378 /**
4379 * @author Vova Feldman (@svovaf)
4380 * @since 1.2.1.7
4381 *
4382 * @param string $email
4383 *
4384 * @return bool
4385 */
4386 static function is_valid_email( $email ) {
4387 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4388 return false;
4389 }
4390
4391 $parts = explode( '@', $email );
4392
4393 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4394 return false;
4395 }
4396
4397 $blacklist = array(
4398 'admin.',
4399 'webmaster.',
4400 'localhost.',
4401 'dev.',
4402 'development.',
4403 'test.',
4404 'stage.',
4405 'staging.',
4406 );
4407
4408 // Make sure domain is not one of the blacklisted.
4409 foreach ( $blacklist as $invalid ) {
4410 if ( 0 === strpos( $parts[1], $invalid ) ) {
4411 return false;
4412 }
4413 }
4414
4415 // Get the UTF encoded domain name.
4416 /**
4417 * @note - The check of `defined('...')` is there to account for PHP servers compiled with some older version of ICU where the constants are not defined.
4418 * @author - @swashata
4419 */
4420 $is_new_idn_available = (
4421 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4422 defined( 'IDNA_DEFAULT' ) &&
4423 defined( 'INTL_IDNA_VARIANT_UTS46' )
4424 );
4425 if ( $is_new_idn_available ) {
4426 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4427 } else {
4428 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4429 }
4430
4431 $domain = $domain . '.';
4432
4433 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4434 }
4435
4436 #endregion
4437
4438 #----------------------------------------------------------------------------------
4439 #region Email
4440 #----------------------------------------------------------------------------------
4441
4442 /**
4443 * Generates and sends an HTML email with customizable sections.
4444 *
4445 * @author Leo Fajardo (@leorw)
4446 * @since 1.1.2
4447 *
4448 * @param string $to_address
4449 * @param string $subject
4450 * @param array $sections
4451 * @param array $headers
4452 *
4453 * @return bool Whether the email contents were sent successfully.
4454 */
4455 private function send_email(
4456 $to_address,
4457 $subject,
4458 $sections = array(),
4459 $headers = array()
4460 ) {
4461 $default_sections = $this->get_email_sections();
4462
4463 // Insert new sections or replace the default email sections.
4464 if ( is_array( $sections ) && ! empty( $sections ) ) {
4465 foreach ( $sections as $section_id => $custom_section ) {
4466 if ( ! isset( $default_sections[ $section_id ] ) ) {
4467 // If the section does not exist, add it.
4468 $default_sections[ $section_id ] = $custom_section;
4469 } else {
4470 // If the section already exists, override it.
4471 $current_section = $default_sections[ $section_id ];
4472
4473 // Replace the current section's title if a custom section title exists.
4474 if ( isset( $custom_section['title'] ) ) {
4475 $current_section['title'] = $custom_section['title'];
4476 }
4477
4478 // Insert new rows under the current section or replace the default rows.
4479 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4480 foreach ( $custom_section['rows'] as $row_id => $row ) {
4481 $current_section['rows'][ $row_id ] = $row;
4482 }
4483 }
4484
4485 $default_sections[ $section_id ] = $current_section;
4486 }
4487 }
4488 }
4489
4490 $vars = array( 'sections' => $default_sections );
4491 $message = fs_get_template( 'email.php', $vars );
4492
4493 // Set the type of email to HTML.
4494 $headers[] = 'Content-type: text/html; charset=UTF-8';
4495
4496 $header_string = implode( "\r\n", $headers );
4497
4498 return wp_mail(
4499 $to_address,
4500 $subject,
4501 $message,
4502 $header_string
4503 );
4504 }
4505
4506 /**
4507 * Generates the data for the sections of the email content.
4508 *
4509 * @author Leo Fajardo (@leorw)
4510 * @since 1.1.2
4511 *
4512 * @return array
4513 */
4514 private function get_email_sections() {
4515 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4516 $current_user = self::_get_current_wp_user();
4517
4518 // Retrieve the cURL version information so that we can get the version number below.
4519 $curl_version_information = curl_version();
4520
4521 $active_plugin = self::get_active_plugins();
4522
4523 // Generate the list of active plugins separated by new line.
4524 $active_plugin_string = '';
4525 foreach ( $active_plugin as $plugin ) {
4526 $active_plugin_string .= sprintf(
4527 '<a href="%s">%s</a> [v%s]<br>',
4528 $plugin['PluginURI'],
4529 $plugin['Name'],
4530 $plugin['Version']
4531 );
4532 }
4533
4534 $server_ip = WP_FS__REMOTE_ADDR;
4535
4536 // Add PHP info for deeper investigation.
4537 ob_start();
4538 phpinfo();
4539 $php_info = ob_get_clean();
4540
4541 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4542
4543 // Generate the default email sections.
4544 $sections = array(
4545 'sdk' => array(
4546 'title' => 'SDK',
4547 'rows' => array(
4548 'fs_version' => array( 'FS Version', $this->version ),
4549 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4550 )
4551 ),
4552 'plugin' => array(
4553 'title' => ucfirst( $this->get_module_type() ),
4554 'rows' => array(
4555 'name' => array( 'Name', $this->get_plugin_name() ),
4556 'version' => array( 'Version', $this->get_plugin_version() )
4557 )
4558 ),
4559 'api' => array(
4560 'title' => 'API Subdomain',
4561 'rows' => array(
4562 'dns' => array(
4563 'DNS_CNAME',
4564 function_exists( 'dns_get_record' ) ?
4565 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4566 'dns_get_record() disabled/blocked'
4567 ),
4568 'ip' => array(
4569 'IP',
4570 function_exists( 'gethostbyname' ) ?
4571 gethostbyname( $api_domain ) :
4572 'gethostbyname() disabled/blocked'
4573 ),
4574 ),
4575 ),
4576 'site' => array(
4577 'title' => 'Site',
4578 'rows' => array(
4579 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4580 'address' => array( 'Address', site_url() ),
4581 'host' => array(
4582 'HTTP_HOST',
4583 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4584 ),
4585 'hosting' => array(
4586 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4587 fs_request_get( 'hosting_company' ) :
4588 'Unknown',
4589 ),
4590 'server_addr' => array(
4591 'SERVER_ADDR',
4592 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4593 )
4594 )
4595 ),
4596 'user' => array(
4597 'title' => 'User',
4598 'rows' => array(
4599 'email' => array( 'Email', $current_user->user_email ),
4600 'first' => array( 'First', $current_user->user_firstname ),
4601 'last' => array( 'Last', $current_user->user_lastname )
4602 )
4603 ),
4604 'plugins' => array(
4605 'title' => 'Plugins',
4606 'rows' => array(
4607 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4608 )
4609 ),
4610 'php_info' => array(
4611 'title' => 'PHP Info',
4612 'rows' => array(
4613 'info' => array( $php_info )
4614 ),
4615 )
4616 );
4617
4618 // Allow the sections to be modified by other code.
4619 $sections = $this->apply_filters( 'email_template_sections', $sections );
4620
4621 return $sections;
4622 }
4623
4624 #endregion
4625
4626 #----------------------------------------------------------------------------------
4627 #region Initialization
4628 #----------------------------------------------------------------------------------
4629
4630 /**
4631 * Init plugin's Freemius instance.
4632 *
4633 * @author Vova Feldman (@svovaf)
4634 * @since 1.0.1
4635 *
4636 * @param number $id
4637 * @param string $public_key
4638 * @param bool $is_live
4639 * @param bool $is_premium
4640 */
4641 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4642 $this->_logger->entrance();
4643
4644 $this->dynamic_init( array(
4645 'id' => $id,
4646 'public_key' => $public_key,
4647 'is_live' => $is_live,
4648 'is_premium' => $is_premium,
4649 ) );
4650 }
4651
4652 /**
4653 * Dynamic initiator, originally created to support initiation
4654 * with parent_id for add-ons.
4655 *
4656 * @author Vova Feldman (@svovaf)
4657 * @since 1.0.6
4658 *
4659 * @param array $plugin_info
4660 *
4661 * @throws Freemius_Exception
4662 */
4663 function dynamic_init( array $plugin_info ) {
4664 $this->_logger->entrance();
4665
4666 $this->parse_settings( $plugin_info );
4667
4668 $this->register_after_settings_parse_hooks();
4669
4670 /**
4671 * 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.
4672 *
4673 * @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.
4674 */
4675 if ( $this->is_anonymous() ) {
4676 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4677
4678 if (
4679 ! $is_network_level ||
4680 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4681 ) {
4682 if ( $this->is_paying_or_trial() ) {
4683 $this->reset_anonymous_mode( $is_network_level );
4684 }
4685 } else {
4686 $network = get_network();
4687
4688 if ( is_object( $network ) ) {
4689 $main_blog_id = $network->site_id;
4690 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4691
4692 if ( is_object( $first_install ) ) {
4693 $this->_storage->network_install_blog_id = $main_blog_id;
4694 $this->_storage->network_user_id = $first_install->user_id;
4695 }
4696 }
4697 }
4698 }
4699
4700 if ( $this->should_stop_execution() ) {
4701 return;
4702 }
4703
4704 if ( ! $this->is_registered() ) {
4705 if ( $this->is_anonymous() ) {
4706 // If user skipped, no need to test connectivity.
4707 $this->_has_api_connection = true;
4708 $this->_is_on = true;
4709 } else {
4710 if ( false === $this->has_api_connectivity() ) {
4711 return;
4712 } else {
4713 if ( $this->_anonymous_mode ) {
4714 // Simulate anonymous mode.
4715 $this->_is_anonymous = true;
4716 }
4717 }
4718 }
4719 }
4720
4721 /**
4722 * This should be executed even if Freemius is off for the core module,
4723 * otherwise, the add-ons dialog box won't work properly. This is especially
4724 * relevant when the developer decided to turn FS off for existing users.
4725 *
4726 * @author Vova Feldman (@svovaf)
4727 */
4728 if ( $this->is_user_in_admin() &&
4729 'plugin-information' === fs_request_get( 'tab', false ) &&
4730 $this->should_use_freemius_updater_and_dialog() &&
4731 (
4732 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4733 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4734 )
4735 ) {
4736 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4737
4738 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4739 }
4740
4741 // Check if Freemius is on for the current plugin.
4742 // This MUST be executed after all the plugin variables has been loaded.
4743 if ( ! $this->is_registered() && ! $this->is_on() ) {
4744 return;
4745 }
4746
4747 if ( $this->has_api_connectivity() ) {
4748 if ( self::is_cron() ) {
4749 $this->hook_callback_to_sync_cron();
4750 } else if ( $this->is_user_in_admin() ) {
4751 /**
4752 * Schedule daily data sync cron if:
4753 *
4754 * 1. User opted-in (for tracking).
4755 * 2. If skipped, but later upgraded (opted-in via upgrade).
4756 *
4757 * @author Vova Feldman (@svovaf)
4758 * @since 1.1.7.3
4759 *
4760 */
4761 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4762 $this->maybe_schedule_sync_cron();
4763 }
4764
4765 /**
4766 * Check if requested for manual blocking background sync.
4767 */
4768 if ( fs_request_has( 'background_sync' ) ) {
4769 self::require_pluggable_essentials();
4770 self::wp_cookie_constants();
4771
4772 $this->run_manual_sync();
4773 }
4774 }
4775 }
4776
4777 if ( $this->is_registered() ) {
4778 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4779
4780 $this->hook_callback_to_install_sync();
4781 }
4782
4783 if ( $this->is_addon() ) {
4784 if ( $this->is_parent_plugin_installed() ) {
4785 // Link to parent FS.
4786 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4787
4788 // Get parent plugin reference.
4789 $this->_parent_plugin = $this->_parent->get_plugin();
4790 }
4791 }
4792
4793 if ( $this->is_user_in_admin() ) {
4794 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4795 $this->_admin_notices->add_sticky(
4796 sprintf(
4797 /* translators: %s: License type (e.g. you have a professional license) */
4798 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4799 fs_request_get( 'purchased_plan' )
4800 ) .
4801 sprintf(
4802 $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' ),
4803 $this->get_module_label( true ),
4804 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4805 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4806 sprintf(
4807 '<strong>%s</strong>',
4808 fs_request_get( 'purchase_email' )
4809 )
4810 ),
4811 'plan_purchased',
4812 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4813 );
4814 }
4815
4816 if ( $this->is_addon() ) {
4817 if ( ! $this->is_parent_plugin_installed() ) {
4818 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4819
4820 if ( isset( $plugin_info['parent'] ) ) {
4821 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4822 }
4823
4824 $this->_admin_notices->add(
4825 ( ! empty( $parent_name ) ?
4826 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 ) :
4827 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() )
4828 ),
4829 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4830 'error'
4831 );
4832
4833 return;
4834 } else {
4835 $is_network_admin = fs_is_network_admin();
4836
4837 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4838 // If add-on activated and parent not, automatically install parent for the user.
4839 $this->activate_parent_account( $this->_parent );
4840 } else if (
4841 $this->_parent->is_registered() &&
4842 ! $this->is_registered() &&
4843 /**
4844 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4845 * * Network active and in network admin - network activate add-on account.
4846 * * Network active and not in network admin - activate add-on account for the current blog.
4847 * * Not network active and not in network admin - activate add-on account for the current blog.
4848 *
4849 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4850 *
4851 * @author Leo Fajardo (@leorw)
4852 * @since 2.3.0
4853 */
4854 ( $this->is_network_active() || ! $is_network_admin )
4855 ) {
4856 $premium_license = null;
4857
4858 if (
4859 ! $this->has_free_plan() &&
4860 $this->is_bundle_license_auto_activation_enabled() &&
4861 $this->_parent->is_activated_with_bundle_license()
4862 ) {
4863 /**
4864 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4865 *
4866 * @author Leo Fajardo (@leorw)
4867 * @since 2.4.0
4868 */
4869 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4870
4871 if (
4872 is_object( $bundle_license ) &&
4873 ! empty( $bundle_license->products ) &&
4874 in_array( $this->get_id(), $bundle_license->products )
4875 ) {
4876 $premium_license = $bundle_license;
4877 }
4878 }
4879
4880 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4881 // If parent plugin activated, automatically install add-on for the user.
4882 $this->_activate_addon_account(
4883 $this->_parent,
4884 ( $this->is_network_active() && $is_network_admin ) ?
4885 true :
4886 get_current_blog_id(),
4887 $premium_license
4888 );
4889 }
4890 }
4891
4892 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4893 if ( $this->is_premium() ) {
4894 // Remove add-on download admin-notice.
4895 $this->_parent->_admin_notices->remove_sticky( array(
4896 'addon_plan_upgraded_' . $this->_slug,
4897 'no_addon_license_' . $this->_slug,
4898 ) );
4899 }
4900
4901 // $this->deactivate_premium_only_addon_without_license();
4902 }
4903 }
4904
4905 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4906
4907 // if ( $this->is_registered() ||
4908 // $this->is_anonymous() ||
4909 // $this->is_pending_activation()
4910 // ) {
4911 // $this->_init_admin();
4912 // }
4913 }
4914
4915 /**
4916 * Should be called outside `$this->is_user_in_admin()` scope
4917 * because the updater has some logic that needs to be executed
4918 * during AJAX calls.
4919 *
4920 * Currently, we need to hook to the `http_request_host_is_external` filter.
4921 * In the future, there might be additional logic added.
4922 *
4923 * @author Vova Feldman
4924 * @since 1.2.1.6
4925 */
4926 if (
4927 $this->should_use_freemius_updater_and_dialog() &&
4928 (
4929 $this->is_premium() ||
4930 /**
4931 * If not premium but the premium version is installed, also instantiate the updater so that the
4932 * plugin information dialog of the premium version will have the information from the server.
4933 *
4934 * @author Leo Fajardo (@leorw)
4935 * @since 2.2.3
4936 */
4937 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4938 ) &&
4939 $this->has_release_on_freemius() &&
4940 ( ! $this->is_unresolved_clone( true ) )
4941 ) {
4942 FS_Plugin_Updater::instance( $this );
4943 }
4944
4945 $this->do_action( 'initiated' );
4946
4947 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4948 if ( isset( $this->_storage->prev_is_premium ) ) {
4949 $this->apply_filters(
4950 'after_code_type_change',
4951 // New code type.
4952 $this->_plugin->is_premium
4953 );
4954 } else {
4955 // Set for code type for the first time.
4956 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4957 }
4958 }
4959
4960 if ( ! $this->is_addon() ) {
4961 if ( $this->is_registered() ) {
4962 // Fix for upgrade from versions < 1.0.9.
4963 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4964 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4965 }
4966
4967 $this->do_action( 'after_init_plugin_registered' );
4968 } else if ( $this->is_anonymous() ) {
4969 $this->do_action( 'after_init_plugin_anonymous' );
4970 } else if ( $this->is_pending_activation() ) {
4971 $this->do_action( 'after_init_plugin_pending_activations' );
4972 }
4973 } else {
4974 if ( $this->is_registered() ) {
4975 $this->do_action( 'after_init_addon_registered' );
4976 } else if ( $this->is_anonymous() ) {
4977 $this->do_action( 'after_init_addon_anonymous' );
4978 } else if ( $this->is_pending_activation() ) {
4979 $this->do_action( 'after_init_addon_pending_activations' );
4980 }
4981 }
4982 }
4983
4984 /**
4985 * @author Leo Fajardo (@leorw)
4986 * @since 2.2.3
4987 *
4988 * @return bool
4989 */
4990 private function should_use_freemius_updater_and_dialog() {
4991 return (
4992 /**
4993 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
4994 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
4995 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
4996 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
4997 * plugin details from .org).
4998 */
4999 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5000 (
5001 ! self::is_plugin_install_page() &&
5002 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5003 ( 'install-plugin' !== fs_request_get( 'action' ) )
5004 )
5005 );
5006 }
5007
5008 /**
5009 * @param string[] $permissions
5010 * @param bool $is_enabled
5011 * @param int|null $blog_id
5012 *
5013 * @return true|object `true` on success, API error object on failure.
5014 */
5015 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
5016 $this->_logger->entrance();
5017
5018 $params = array(
5019 'permissions' => implode( ',', $permissions ),
5020 'is_enabled' => $is_enabled,
5021 );
5022
5023 $current_blog_id = get_current_blog_id();
5024 $is_blog_switched = false;
5025 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
5026 $is_blog_switched = $this->switch_to_blog( $blog_id );
5027 }
5028
5029 $result = $this->api_site_call( '/permissions.json', 'put', $params );
5030
5031 if ( $is_blog_switched ) {
5032 $this->switch_to_blog( $current_blog_id );
5033 }
5034
5035 if (
5036 ! $this->is_api_result_object( $result ) ||
5037 ! isset( $result->install_id )
5038 ) {
5039 $this->_logger->api_error( $result );
5040
5041 return $result;
5042 }
5043
5044 return true;
5045 }
5046
5047 /**
5048 * @param string[] $permissions
5049 * @param bool $is_enabled
5050 * @param bool $has_site_delegated_connection
5051 *
5052 * @return true|object `true` on success, API error object on failure.
5053 */
5054 private function update_network_permissions(
5055 array $permissions,
5056 $is_enabled,
5057 &$has_site_delegated_connection
5058 ) {
5059 $this->_logger->entrance();
5060
5061 $install_id_2_blog_id = array();
5062 $install_by_blog_id = $this->get_blog_install_map();
5063
5064 $has_site_delegated_connection = false;
5065
5066 foreach ( $install_by_blog_id as $blog_id => $install ) {
5067 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5068 // Only update permissions of non-delegated installs.
5069 $has_site_delegated_connection = true;
5070 continue;
5071 }
5072
5073 $install_id_2_blog_id[ $install->id ] = $blog_id;
5074 }
5075
5076 if ( empty( $install_id_2_blog_id ) ) {
5077 return true;
5078 }
5079
5080 $params = array(
5081 'permissions' => implode( ',', $permissions ),
5082 'is_enabled' => $is_enabled,
5083 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
5084 );
5085
5086 // Send update to FS.
5087 $result = $this->get_current_or_network_user_api_scope()->call(
5088 "/plugins/{$this->_module_id}/installs/permissions.json",
5089 'put',
5090 $params
5091 );
5092
5093 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
5094 $this->_logger->api_error( $result );
5095
5096 return $result;
5097 }
5098
5099 return true;
5100 }
5101
5102 /**
5103 * @param mixed $result
5104 *
5105 * @return string
5106 */
5107 private function get_api_error_message( $result ) {
5108 $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:',
5109 'unexpected-api-error' ), $this->_module_type ) . ' ';
5110
5111 if (
5112 $this->is_api_error( $result ) &&
5113 isset( $result->error )
5114 ) {
5115 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
5116
5117 $error_message .= "<b>{$result->error->message}{$code}</b>";
5118 } else {
5119 $error_message .= var_export( $result, true );
5120 }
5121
5122 return $error_message;
5123 }
5124
5125 /**
5126 * @author Vova Feldman (@svovaf)
5127 * @since 2.5.1
5128 */
5129 function _toggle_permission_tracking_callback() {
5130 $this->_logger->entrance();
5131
5132 $this->check_ajax_referer( 'toggle_permission_tracking' );
5133
5134 if ( ! $this->is_registered( true ) ) {
5135 self::shoot_ajax_failure( 'User never opted-in.' );
5136 }
5137
5138 $is_enabled = fs_request_get_bool( 'is_enabled' );
5139 $permissions = fs_request_get( 'permissions' );
5140
5141 if ( ! is_string( $permissions ) ) {
5142 self::shoot_ajax_failure( 'The permissions param must be a string.' );
5143 }
5144
5145 $permissions = explode( ',', $permissions );
5146
5147 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
5148
5149 if ( true !== $result ) {
5150 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
5151 }
5152
5153 self::shoot_ajax_success();
5154 }
5155
5156 /**
5157 * @param string[] $permissions
5158 * @param bool $is_enabled
5159 * @param int|null $blog_id
5160 *
5161 * @return bool|mixed `true` if updated successfully or no update is needed.
5162 */
5163 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
5164 if ( ! $this->is_registered( true ) ) {
5165 // User never opted-in.
5166 return false;
5167 }
5168
5169 // Check if permissions are already set as needed.
5170 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
5171 /**
5172 * Note:
5173 * 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.
5174 */
5175 return true;
5176 }
5177
5178 $api_managed_permissions = array_intersect(
5179 $permissions,
5180 FS_Permission_Manager::get_api_managed_permission_ids()
5181 );
5182
5183 if (
5184 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
5185 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
5186 ) {
5187 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
5188 }
5189
5190 if ( ! empty( $api_managed_permissions ) ) {
5191 $has_site_delegated_connection = false;
5192
5193 if (
5194 ! $is_enabled &&
5195 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
5196 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
5197 ) {
5198 /**
5199 * 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.
5200 *
5201 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
5202 */
5203 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
5204 }
5205
5206 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
5207 $result = $this->update_network_permissions(
5208 $api_managed_permissions,
5209 $is_enabled,
5210 $has_site_delegated_connection
5211 );
5212 } else {
5213 $result = $this->update_site_permissions(
5214 $api_managed_permissions,
5215 $is_enabled,
5216 $blog_id
5217 );
5218 }
5219
5220 if ( true !== $result ) {
5221 return $result;
5222 }
5223
5224 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
5225 if ( $is_enabled ) {
5226 $this->schedule_sync_cron();
5227 } else {
5228 $this->clear_sync_cron( ! $has_site_delegated_connection );
5229 }
5230 }
5231
5232 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
5233 $this->toggle_user_permission( $is_enabled, $blog_id );
5234 }
5235 }
5236
5237 $this->update_tracking_permissions(
5238 $permissions,
5239 $is_enabled,
5240 $blog_id
5241 );
5242
5243 return true;
5244 }
5245
5246 /**
5247 * @param bool $is_enabled
5248 * @param int|null $blog_id
5249 */
5250 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5251 $network_or_blog_ids = is_numeric( $blog_id ) ?
5252 $blog_id :
5253 fs_is_network_admin();
5254
5255 if ( $is_enabled ) {
5256 $this->reset_anonymous_mode( $network_or_blog_ids );
5257 } else {
5258 $this->skip_connection( $network_or_blog_ids );
5259 }
5260 }
5261
5262 /**
5263 * Opt-in back into usage tracking.
5264 *
5265 * Note: This will only work if the user opted-in previously.
5266 *
5267 * Returns:
5268 * 1. FALSE - If the user never opted-in.
5269 * 2. TRUE - If successfully opted-in back to usage tracking.
5270 * 3. object - API result on failure.
5271 *
5272 * @author Leo Fajardo (@leorw)
5273 * @since 1.2.1.5
5274 *
5275 * @bool $is_enabled
5276 *
5277 * @return bool|object
5278 */
5279 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5280 $this->_logger->entrance();
5281
5282 return $this->toggle_permission_tracking(
5283 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5284 $is_enabled,
5285 $blog_id
5286 );
5287 }
5288
5289 /**
5290 * If user opted-in and later disabled usage-tracking,
5291 * re-allow tracking for licensing and updates.
5292 *
5293 * @author Leo Fajardo (@leorw)
5294 * @since 1.2.1.5
5295 *
5296 * @param bool $is_context_single_site
5297 */
5298 private function reconnect_locally( $is_context_single_site = false ) {
5299 $this->_logger->entrance();
5300
5301 if ( ! $this->is_registered() ) {
5302 return;
5303 }
5304
5305 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5306 if ( $this->is_tracking_prohibited() ) {
5307 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5308 }
5309 } else {
5310 $installs_map = $this->get_blog_install_map();
5311 foreach ( $installs_map as $blog_id => $install ) {
5312 /**
5313 * @var FS_Site $install
5314 */
5315 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5316 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5317 }
5318 }
5319 }
5320 }
5321
5322 /**
5323 * 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.
5324 *
5325 * @param string[] $permissions
5326 * @param bool $is_enabled
5327 * @param int|null $blog_id
5328 *
5329 * @return array
5330 */
5331 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5332 // Alias.
5333 $permission_manager = FS_Permission_Manager::instance( $this );
5334
5335 $network_or_blog_ids = is_numeric( $blog_id ) ?
5336 $blog_id :
5337 fs_is_network_admin();
5338
5339 if ( true === $network_or_blog_ids ) {
5340 // Update the permission for all non-delegated sub-sites.
5341 $blog_ids = $this->get_non_delegated_blog_ids();
5342
5343 // Add the network-level to the array, to update the permission on the network-level storage.
5344 array_unshift( $blog_ids, null );
5345 }
5346 else
5347 {
5348 if ( false === $network_or_blog_ids ) {
5349 $network_or_blog_ids = null;
5350 }
5351
5352 $blog_ids = is_array( $network_or_blog_ids ) ?
5353 $network_or_blog_ids :
5354 array( $network_or_blog_ids );
5355 }
5356
5357 $result = array();
5358 foreach ( $permissions as $permission ) {
5359 $permission = trim( $permission );
5360 $is_permission_supported = true;
5361
5362 foreach ( $blog_ids as $id ) {
5363 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5364 $permission,
5365 $is_enabled,
5366 $id
5367 );
5368 }
5369
5370 if ( ! $is_permission_supported ) {
5371 $permission = 'no_match';
5372 }
5373
5374 $result[ $permission ] = $is_enabled;
5375 }
5376
5377 return $result;
5378 }
5379
5380 /**
5381 * Parse plugin's settings (as defined by the plugin dev).
5382 *
5383 * @author Vova Feldman (@svovaf)
5384 * @since 1.1.7.3
5385 *
5386 * @param array $plugin_info
5387 *
5388 * @throws \Freemius_Exception
5389 */
5390 private function parse_settings( &$plugin_info ) {
5391 $this->_logger->entrance();
5392
5393 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5394 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5395 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5396 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5397 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5398
5399 /**
5400 * @author Vova Feldman (@svovaf)
5401 * @since 1.1.9 Try to pull secret key from external config.
5402 */
5403 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5404 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5405 }
5406
5407 if ( isset( $plugin_info['parent'] ) ) {
5408 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5409 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5410 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5411 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5412 }
5413
5414 if ( false === $id ) {
5415 throw new Freemius_Exception( array(
5416 'error' => array(
5417 'type' => 'ParameterNotSet',
5418 'message' => 'Plugin id parameter is not set.',
5419 'code' => 'plugin_id_not_set',
5420 'http' => 500,
5421 )
5422 ) );
5423 }
5424 if ( false === $public_key ) {
5425 throw new Freemius_Exception( array(
5426 'error' => array(
5427 'type' => 'ParameterNotSet',
5428 'message' => 'Plugin public_key parameter is not set.',
5429 'code' => 'plugin_public_key_not_set',
5430 'http' => 500,
5431 )
5432 ) );
5433 }
5434
5435 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5436 $this->_plugin :
5437 new FS_Plugin();
5438
5439 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5440
5441 $plugin->update( array(
5442 'id' => $id,
5443 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5444 'public_key' => $public_key,
5445 'slug' => $this->_slug,
5446 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5447 'parent_plugin_id' => $parent_id,
5448 'version' => $this->get_plugin_version(),
5449 'title' => $this->get_plugin_name( $premium_suffix ),
5450 'file' => $this->_plugin_basename,
5451 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5452 'premium_suffix' => $premium_suffix,
5453 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5454 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5455 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5456 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5457 'opt_in_moderation' => $this->get_option( $plugin_info, 'opt_in', null ),
5458 ) );
5459
5460 if ( $plugin->is_updated() ) {
5461 // Update plugin details.
5462 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5463 }
5464 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5465 $this->_plugin->secret_key = $secret_key;
5466
5467 /**
5468 * 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).
5469 *
5470 * @author Vova Feldman
5471 * @since 2.4.5
5472 */
5473 if ( $this->is_network_active() && fs_is_network_admin() ) {
5474 if ( isset( $plugin_info['menu_network'] ) &&
5475 is_array( $plugin_info['menu_network'] ) &&
5476 ! empty( $plugin_info['menu_network'] )
5477 ) {
5478 $plugin_info['menu'] = $plugin_info['menu_network'];
5479 }
5480 }
5481
5482 if ( ! isset( $plugin_info['menu'] ) ) {
5483 $plugin_info['menu'] = array();
5484
5485 if ( ! empty( $this->_storage->sdk_last_version ) &&
5486 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5487 ) {
5488 // Backward compatibility to 1.1.2
5489 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5490 $plugin_info['menu_slug'] :
5491 $this->_slug;
5492 }
5493 }
5494
5495 $this->_menu = FS_Admin_Menu_Manager::instance(
5496 $this->_module_id,
5497 $this->_module_type,
5498 $this->get_unique_affix()
5499 );
5500
5501 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5502
5503 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5504 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5505 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5506 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5507 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5508 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5509 if ( $this->_is_premium_only ) {
5510 // If premium only plugin, disable anonymous mode.
5511 $this->_enable_anonymous = false;
5512 $this->_anonymous_mode = false;
5513 } else {
5514 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5515 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5516 }
5517 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5518 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5519
5520 if ( ! empty( $plugin_info['trial'] ) ) {
5521 $this->_trial_days = $this->get_numeric_option(
5522 $plugin_info['trial'],
5523 'days',
5524 // Default to 0 - trial without days specification.
5525 0
5526 );
5527
5528 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5529 }
5530
5531 $this->_navigation = $this->get_option(
5532 $plugin_info,
5533 'navigation',
5534 $this->is_free_wp_org_theme() ?
5535 self::NAVIGATION_TABS :
5536 self::NAVIGATION_MENU
5537 );
5538 }
5539
5540 /**
5541 * @param string[] $options
5542 * @param string $key
5543 * @param mixed $default
5544 *
5545 * @return bool
5546 */
5547 private function get_option( &$options, $key, $default = false ) {
5548 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5549 }
5550
5551 private function get_bool_option( &$options, $key, $default = false ) {
5552 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5553 }
5554
5555 private function get_numeric_option( &$options, $key, $default = false ) {
5556 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5557 }
5558
5559 /**
5560 * Gate keeper.
5561 *
5562 * @author Vova Feldman (@svovaf)
5563 * @since 1.1.7.3
5564 *
5565 * @return bool
5566 */
5567 private function should_stop_execution() {
5568 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5569 /**
5570 * Don't execute Freemius until plugin was fully loaded at least once,
5571 * to give the opportunity for the activation hook to run before pinging
5572 * the API for connectivity test. This logic is relevant for the
5573 * identification of new plugin install vs. plugin update.
5574 *
5575 * @author Vova Feldman (@svovaf)
5576 * @since 1.1.9
5577 */
5578 return true;
5579 }
5580
5581 if ( $this->is_activation_mode() ) {
5582 if ( ! is_admin() ) {
5583 /**
5584 * If in activation mode, don't execute Freemius outside the admin dashboard.
5585 *
5586 * @author Vova Feldman (@svovaf)
5587 * @since 1.1.7.3
5588 */
5589 return true;
5590 }
5591
5592 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5593 /**
5594 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5595 * then don't start Freemius.
5596 *
5597 * @author Vova Feldman (@svovaf)
5598 * @since 1.1.6.3
5599 *
5600 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5601 */
5602 return true;
5603 }
5604
5605 if ( self::is_cron() ) {
5606 /**
5607 * If in activation mode, don't execute Freemius during wp crons
5608 * (wp crons have HTTP context - called as HTTP request).
5609 *
5610 * @author Vova Feldman (@svovaf)
5611 * @since 1.1.7.3
5612 */
5613 return true;
5614 }
5615
5616 if ( self::is_ajax() ) {
5617 /**
5618 * During activation, if running in AJAX mode, unless there's a sticky
5619 * connectivity issue notice, don't run Freemius.
5620 *
5621 * @author Vova Feldman (@svovaf)
5622 * @since 1.1.7.3
5623 */
5624 return true;
5625 }
5626 }
5627
5628 return false;
5629 }
5630
5631 /**
5632 * Triggered after code type has changed.
5633 *
5634 * @author Vova Feldman (@svovaf)
5635 * @since 1.1.9.1
5636 */
5637 function _after_code_type_change() {
5638 $this->_logger->entrance();
5639
5640 if ( $this->is_theme() ) {
5641 // Expire the cache of the previous tabs since the theme may
5642 // have setting updates after code type has changed.
5643 $this->_cache->expire( 'tabs' );
5644 $this->_cache->expire( 'tabs_stylesheets' );
5645 }
5646
5647 if ( ! $this->is_addon() ) {
5648 add_action(
5649 is_admin() ? 'admin_init' : 'init',
5650 array( &$this, '_plugin_code_type_changed' )
5651 );
5652 }
5653
5654 if ( $this->is_registered() && $this->is_premium() ) {
5655 // Purge cached payments after switching to the premium version.
5656 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5657 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5658 }
5659 }
5660
5661 /**
5662 * Handles plugin's code type change (free <--> premium).
5663 *
5664 * @author Vova Feldman (@svovaf)
5665 * @since 1.0.9
5666 */
5667 function _plugin_code_type_changed() {
5668 $this->_logger->entrance();
5669
5670 if ( $this->is_premium() ) {
5671 $this->reconnect_locally();
5672
5673 // Activated premium code.
5674 $this->do_action( 'after_premium_version_activation' );
5675
5676 // Remove all sticky messages related to download of the premium version.
5677 $this->_admin_notices->remove_sticky( array(
5678 'trial_started',
5679 'plan_upgraded',
5680 'plan_changed',
5681 'license_activated',
5682 ) );
5683
5684 $notice = '';
5685 if ( ! $this->is_only_premium() ) {
5686 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5687 }
5688
5689 $license_notice = $this->get_license_network_activation_notice();
5690 if ( ! empty( $license_notice ) ) {
5691 $notice .= ' ' . $license_notice;
5692 }
5693
5694 if ( ! empty( $notice ) ) {
5695 $this->_admin_notices->add_sticky(
5696 trim( $notice ),
5697 'premium_activated',
5698 $this->get_text_x_inline( 'W00t',
5699 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5700 );
5701 }
5702 } else {
5703 // Remove sticky message related to premium code activation.
5704 $this->_admin_notices->remove_sticky( 'premium_activated' );
5705
5706 // Activated free code (after had the premium before).
5707 $this->do_action( 'after_free_version_reactivation' );
5708
5709 if ( $this->is_paying() && ! $this->is_premium() ) {
5710 $this->add_complete_upgrade_instructions_notice(
5711 sprintf(
5712 /* translators: %s: License type (e.g. you have a professional license) */
5713 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5714 $this->get_plan_title()
5715 ),
5716 'plan_upgraded'
5717 );
5718 }
5719 }
5720
5721 if ( $this->is_registered() ) {
5722 // Schedule code type changes event.
5723 $this->schedule_install_sync();
5724 }
5725
5726 /**
5727 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5728 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5729 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5730 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5731 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5732 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5733 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5734 */
5735 $this->unregister_uninstall_hook();
5736
5737 $this->clear_module_main_file_cache();
5738
5739 // Update is_premium of latest version.
5740 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5741 }
5742
5743 #endregion
5744
5745 #----------------------------------------------------------------------------------
5746 #region Add-ons
5747 #----------------------------------------------------------------------------------
5748
5749 /**
5750 * Check if add-on installed and activated on site.
5751 *
5752 * @author Vova Feldman (@svovaf)
5753 * @since 1.0.6
5754 *
5755 * @param string|number $id_or_slug
5756 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5757 *
5758 * @return bool
5759 */
5760 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5761 $this->_logger->entrance();
5762
5763 $addon_id = self::get_module_id( $id_or_slug );
5764 $is_activated = self::has_instance( $addon_id );
5765
5766 if ( ! $is_activated ) {
5767 return false;
5768 }
5769
5770 if ( is_bool( $is_premium ) ) {
5771 // Check if the specified code version is activate.
5772 $addon = $this->get_addon_instance( $addon_id );
5773 $is_activated = ( $is_premium === $addon->is_premium() );
5774 }
5775
5776 return $is_activated;
5777 }
5778
5779 /**
5780 * Check if add-on was connected to install
5781 *
5782 * @author Vova Feldman (@svovaf)
5783 * @since 1.1.7
5784 *
5785 * @param string|number $id_or_slug
5786 *
5787 * @return bool
5788 */
5789 function is_addon_connected( $id_or_slug ) {
5790 $this->_logger->entrance();
5791
5792 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5793
5794 $addon_id = self::get_module_id( $id_or_slug );
5795 $addon = $this->get_addon( $addon_id );
5796 $slug = $addon->slug;
5797 if ( ! isset( $sites[ $slug ] ) ) {
5798 return false;
5799 }
5800
5801 $site = $sites[ $slug ];
5802
5803 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5804
5805 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5806 // The given slug do NOT belong to any of the plugin's add-ons.
5807 return false;
5808 }
5809
5810 return ( is_object( $site ) &&
5811 is_numeric( $site->id ) &&
5812 is_numeric( $site->user_id ) &&
5813 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5814 );
5815 }
5816
5817 /**
5818 * Determines if add-on installed.
5819 *
5820 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5821 *
5822 * @author Vova Feldman (@svovaf)
5823 * @since 1.0.6
5824 *
5825 * @param string|number $id_or_slug
5826 *
5827 * @return bool
5828 */
5829 function is_addon_installed( $id_or_slug ) {
5830 $this->_logger->entrance();
5831
5832 $addon_id = self::get_module_id( $id_or_slug );
5833
5834 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5835 }
5836
5837 /**
5838 * Get add-on basename.
5839 *
5840 * @author Vova Feldman (@svovaf)
5841 * @since 1.0.6
5842 *
5843 * @param string|number $id_or_slug
5844 *
5845 * @return string
5846 */
5847 function get_addon_basename( $id_or_slug ) {
5848 $addon_id = self::get_module_id( $id_or_slug );
5849
5850 if ( $this->is_addon_activated( $addon_id ) ) {
5851 return self::instance( $addon_id )->get_plugin_basename();
5852 }
5853
5854 $addon = $this->get_addon( $addon_id );
5855 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5856
5857 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5858 return $premium_basename;
5859 }
5860
5861 $all_plugins = $this->get_all_plugins();
5862
5863 foreach ( $all_plugins as $basename => $data ) {
5864 if ( $addon->slug === $data['slug'] ||
5865 $addon->premium_slug === $data['slug']
5866 ) {
5867 return $basename;
5868 }
5869 }
5870
5871 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5872
5873 return $free_basename;
5874 }
5875
5876 /**
5877 * Get installed add-ons instances.
5878 *
5879 * @author Vova Feldman (@svovaf)
5880 * @since 1.0.6
5881 *
5882 * @return Freemius[]
5883 */
5884 function get_installed_addons() {
5885 if ( $this->is_addon() ) {
5886 // Add-on cannot have add-ons.
5887 return array();
5888 }
5889
5890 $installed_addons = array();
5891
5892 foreach ( self::$_instances as $instance ) {
5893 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5894 $installed_addons[] = $instance;
5895 }
5896 }
5897
5898 return $installed_addons;
5899 }
5900
5901 /**
5902 * Check if any add-ons of the plugin are installed.
5903 *
5904 * @author Leo Fajardo (@leorw)
5905 * @since 1.1.1
5906 *
5907 * @return bool
5908 */
5909 function has_installed_addons() {
5910 if ( ! $this->has_addons() ) {
5911 return false;
5912 }
5913
5914 foreach ( self::$_instances as $instance ) {
5915 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5916 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5917 return true;
5918 }
5919 }
5920 }
5921
5922 return false;
5923 }
5924
5925 /**
5926 * Tell Freemius that the current plugin is an add-on.
5927 *
5928 * @author Vova Feldman (@svovaf)
5929 * @since 1.0.6
5930 *
5931 * @param number $parent_plugin_id The parent plugin ID
5932 */
5933 function init_addon( $parent_plugin_id ) {
5934 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5935 }
5936
5937 /**
5938 * @author Vova Feldman (@svovaf)
5939 * @since 1.0.6
5940 *
5941 * @return bool
5942 */
5943 function is_addon() {
5944 return (
5945 isset( $this->_plugin->parent_plugin_id ) &&
5946 is_numeric( $this->_plugin->parent_plugin_id )
5947 );
5948 }
5949
5950 /**
5951 * @author Vova Feldman (@svovaf)
5952 * @since 2.3.2
5953 *
5954 * @param number $parent_product_id
5955 *
5956 * @return bool
5957 */
5958 function is_addon_of( $parent_product_id ) {
5959 return (
5960 $this->is_addon() &&
5961 $parent_product_id == $this->_plugin->parent_plugin_id
5962 );
5963 }
5964
5965 /**
5966 * Deactivate add-on if it's premium only and the user does't have a valid license.
5967 *
5968 * @param bool $is_after_trial_cancel
5969 *
5970 * @return bool If add-on was deactivated.
5971 */
5972 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5973 if ( ! $this->has_free_plan() &&
5974 ! $this->has_features_enabled_license() &&
5975 ! $this->_has_premium_license()
5976 ) {
5977 if ( $this->is_registered() ) {
5978 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5979 // if (empty($this->_storage->activation_timestamp) ||
5980 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5981 // ) {
5982 /**
5983 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5984 *
5985 * Retry syncing the user add-on licenses.
5986 */
5987 // Sync licenses.
5988 $this->_sync_licenses();
5989 // }
5990
5991 // Try to activate premium license.
5992 $this->_activate_license( true );
5993 }
5994
5995 if ( ! $this->has_free_plan() &&
5996 ! $this->has_features_enabled_license() &&
5997 ! $this->_has_premium_license()
5998 ) {
5999 // @todo Check if deactivate plugins also call the deactivation hook.
6000
6001 $this->_parent->_admin_notices->add_sticky(
6002 sprintf(
6003 ( $is_after_trial_cancel ?
6004 $this->_parent->get_text_inline(
6005 '%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.',
6006 'addon-trial-cancelled-message'
6007 ) :
6008 $this->_parent->get_text_inline(
6009 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6010 'addon-no-license-message'
6011 )
6012 ),
6013 '<b>' . $this->_plugin->title . '</b>'
6014 ) . ' ' . sprintf(
6015 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6016 $this->_parent->addon_url( $this->_slug ),
6017 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6018 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6019 ),
6020 'no_addon_license_' . $this->_slug,
6021 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6022 ( $is_after_trial_cancel ? 'success' : 'error' )
6023 );
6024
6025 deactivate_plugins( array( $this->_plugin_basename ), true );
6026
6027 return true;
6028 }
6029 }
6030
6031 return false;
6032 }
6033
6034 #endregion
6035
6036 #----------------------------------------------------------------------------------
6037 #region Sandbox
6038 #----------------------------------------------------------------------------------
6039
6040 /**
6041 * Set Freemius into sandbox mode for debugging.
6042 *
6043 * @author Vova Feldman (@svovaf)
6044 * @since 1.0.4
6045 *
6046 * @param string $secret_key
6047 */
6048 function init_sandbox( $secret_key ) {
6049 $this->_plugin->secret_key = $secret_key;
6050
6051 // Update plugin details.
6052 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6053 }
6054
6055 /**
6056 * Check if running payments in sandbox mode.
6057 *
6058 * @author Vova Feldman (@svovaf)
6059 * @since 1.0.4
6060 *
6061 * @return bool
6062 */
6063 function is_payments_sandbox() {
6064 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6065 }
6066
6067 #endregion
6068
6069 /**
6070 * Check if running test vs. live plugin.
6071 *
6072 * @author Vova Feldman (@svovaf)
6073 * @since 1.0.5
6074 *
6075 * @return bool
6076 */
6077 function is_live() {
6078 return $this->_plugin->is_live;
6079 }
6080
6081 /**
6082 * Check if super-admin skipped connection for all sites in the network.
6083 *
6084 * @author Vova Feldman (@svovaf)
6085 * @since 2.0.0
6086 */
6087 function is_network_anonymous() {
6088 if ( ! $this->_is_network_active ) {
6089 return false;
6090 }
6091
6092 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6093
6094 if ( empty( $is_anonymous_ms ) ) {
6095 return false;
6096 }
6097
6098 return $is_anonymous_ms['is'];
6099 }
6100
6101 /**
6102 * Check if super-admin opted-in for all sites in the network.
6103 *
6104 * @author Vova Feldman (@svovaf)
6105 * @since 2.0.0
6106 */
6107 function is_network_connected() {
6108 if ( ! $this->_is_network_active ) {
6109 return false;
6110 }
6111
6112 return $this->_storage->get( 'is_network_connected' );
6113 }
6114
6115 /**
6116 * Check if the user skipped connecting the account with Freemius.
6117 *
6118 * @author Vova Feldman (@svovaf)
6119 * @since 1.0.7
6120 *
6121 * @return bool
6122 */
6123 function is_anonymous() {
6124 if ( ! isset( $this->_is_anonymous ) ) {
6125 if ( $this->is_network_anonymous() ) {
6126 $this->_is_anonymous = true;
6127 } else if ( fs_is_network_admin() ) {
6128 /**
6129 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
6130 */
6131 $non_delegated_sites = $this->get_non_delegated_blog_ids();
6132
6133 foreach ( $non_delegated_sites as $blog_id ) {
6134 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6135
6136 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
6137 $this->_is_anonymous = false;
6138 break;
6139 }
6140 }
6141
6142 if ( false !== $this->_is_anonymous ) {
6143 $this->_is_anonymous = true;
6144 }
6145 } else {
6146 if ( ! isset( $this->_storage->is_anonymous ) ) {
6147 // Not skipped.
6148 $this->_is_anonymous = false;
6149 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6150 // For back compatibility, since the variable was boolean before.
6151 $this->_is_anonymous = $this->_storage->is_anonymous;
6152
6153 // Upgrade stored data format to 1.1.3 format.
6154 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6155 } else {
6156 // Version 1.1.3 and later.
6157 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6158 }
6159 }
6160 }
6161
6162 return $this->_is_anonymous;
6163 }
6164
6165 /**
6166 * Check if the user skipped the connection of a specified site.
6167 *
6168 * @author Vova Feldman (@svovaf)
6169 * @since 2.0.0
6170 *
6171 * @param int $blog_id
6172 *
6173 * @return bool
6174 */
6175 function is_anonymous_site( $blog_id = 0 ) {
6176 if ( $this->is_network_anonymous() ) {
6177 return true;
6178 }
6179
6180 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6181
6182 if ( empty( $is_anonymous ) ) {
6183 return false;
6184 }
6185
6186 return $is_anonymous['is'];
6187 }
6188
6189 /**
6190 * Check if user connected his account and install pending email activation.
6191 *
6192 * @author Vova Feldman (@svovaf)
6193 * @since 1.0.7
6194 *
6195 * @return bool
6196 */
6197 function is_pending_activation() {
6198 return $this->_storage->get( 'is_pending_activation', false );
6199 }
6200
6201 /**
6202 * @author Leo Fajardo (@leorw)
6203 * @since 2.5.0
6204 */
6205 private function clear_pending_activation_mode() {
6206 // Remove the pending activation sticky notice (if it still exists).
6207 $this->_admin_notices->remove_sticky( 'activation_pending' );
6208
6209 // Clear the plugin's pending activation mode.
6210 unset( $this->_storage->is_pending_activation );
6211 }
6212
6213 /**
6214 * Check if plugin must be WordPress.org compliant.
6215 *
6216 * @since 1.0.7
6217 *
6218 * @return bool
6219 */
6220 function is_org_repo_compliant() {
6221 return $this->_is_org_compliant;
6222 }
6223
6224 #--------------------------------------------------------------------------------
6225 #region WP Cron Common
6226 #--------------------------------------------------------------------------------
6227
6228 /**
6229 * @author Vova Feldman (@svovaf)
6230 * @since 2.0.0
6231 *
6232 * @param string $name Cron name.
6233 *
6234 * @return object
6235 */
6236 private function get_cron_data( $name ) {
6237 $this->_logger->entrance( $name );
6238
6239 /**
6240 * @var object $cron_data
6241 */
6242 return $this->_storage->get( "{$name}_cron", null );
6243 }
6244
6245 /**
6246 * @author Vova Feldman (@svovaf)
6247 * @since 2.0.0
6248 *
6249 * @param string $name Cron name.
6250 */
6251 private function clear_cron_data( $name ) {
6252 $this->_logger->entrance( $name );
6253
6254 $this->_storage->remove( "{$name}_cron" );
6255 }
6256
6257 /**
6258 * @author Vova Feldman (@svovaf)
6259 * @since 2.0.0
6260 *
6261 * @param string $name Cron name.
6262 * @param int $cron_blog_id The cron executing blog ID.
6263 */
6264 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6265 $this->_logger->entrance( $name );
6266
6267 $this->_storage->store( "{$name}_cron", (object) array(
6268 'version' => $this->get_plugin_version(),
6269 'blog_id' => $cron_blog_id,
6270 'sdk_version' => $this->version,
6271 'timestamp' => WP_FS__SCRIPT_START_TIME,
6272 'on' => true,
6273 ) );
6274 }
6275
6276 /**
6277 * Get the cron's executing blog ID.
6278 *
6279 * @author Vova Feldman (@svovaf)
6280 * @since 2.0.0
6281 *
6282 * @param string $name Cron name.
6283 *
6284 * @return int
6285 */
6286 private function get_cron_blog_id( $name ) {
6287 $this->_logger->entrance( $name );
6288
6289 if ( ! is_multisite() ) {
6290 // Not a multisite.
6291 return 0;
6292 }
6293
6294 $cron_data = $this->get_cron_data( $name );
6295
6296 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6297 $cron_data->blog_id :
6298 0;
6299 }
6300
6301 /**
6302 * @author Vova Feldman (@svovaf)
6303 * @since 2.0.0
6304 *
6305 * @param string $name Cron name.
6306 *
6307 * @return bool
6308 */
6309 private function is_cron_on( $name ) {
6310 $this->_logger->entrance( $name );
6311
6312 /**
6313 * @var object $cron_data
6314 */
6315 $cron_data = $this->get_cron_data( $name );
6316
6317 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6318 }
6319
6320 /**
6321 * Unix timestamp for previous cron execution or false if never executed.
6322 *
6323 * @author Vova Feldman (@svovaf)
6324 * @since 2.0.0
6325 *
6326 * @param string $name Cron name.
6327 *
6328 * @return int|false
6329 */
6330 private function cron_last_execution( $name ) {
6331 $this->_logger->entrance( $name );
6332
6333 return $this->_storage->get( "{$name}_timestamp" );
6334 }
6335
6336 /**
6337 * Set cron execution time to now.
6338 *
6339 * @author Vova Feldman (@svovaf)
6340 * @since 2.0.0
6341 *
6342 * @param string $name Cron name.
6343 */
6344 private function set_cron_execution_timestamp( $name ) {
6345 $this->_logger->entrance( $name );
6346
6347 $this->_storage->store( "{$name}_timestamp", time() );
6348 }
6349
6350 /**
6351 * Sets the keepalive time to now.
6352 *
6353 * @author Leo Fajardo (@leorw)
6354 * @since 2.2.3
6355 *
6356 * @param bool|null $use_network_level_storage
6357 */
6358 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6359 $this->_logger->entrance();
6360
6361 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6362 }
6363
6364 /**
6365 * Check if cron was executed in the last $period of seconds.
6366 *
6367 * @author Vova Feldman (@svovaf)
6368 * @since 2.0.0
6369 *
6370 * @param string $name Cron name.
6371 * @param int $period In seconds
6372 *
6373 * @return bool
6374 */
6375 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6376 $this->_logger->entrance( $name );
6377
6378 $last_execution = $this->cron_last_execution( $name );
6379
6380 if ( ! is_numeric( $last_execution ) ) {
6381 return false;
6382 }
6383
6384 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6385 }
6386
6387 /**
6388 * WP Cron is executed on a site level. When running in a multisite network environment
6389 * with the network integration activated, for optimization reasons, we are consolidating
6390 * the installs data sync cron to be executed only from a single site.
6391 *
6392 * @author Vova Feldman (@svovaf)
6393 * @since 2.0.0
6394 *
6395 * @param int $except_blog_id Target any except the excluded blog ID.
6396 *
6397 * @return int
6398 */
6399 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6400 if ( ! is_multisite() ) {
6401 return 0;
6402 }
6403
6404 if ( $this->_is_network_active ) {
6405 $network_install_blog_id = $this->_storage->network_install_blog_id;
6406
6407 if (
6408 is_numeric( $network_install_blog_id ) &&
6409 $except_blog_id != $network_install_blog_id &&
6410 self::is_site_active( $network_install_blog_id )
6411 ) {
6412 // Try to run cron from the main network blog.
6413 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6414
6415 if (
6416 is_object( $install ) &&
6417 $this->is_tracking_allowed( $network_install_blog_id, $install )
6418 ) {
6419 return $network_install_blog_id;
6420 }
6421 }
6422 }
6423
6424 // Get first opted-in blog ID with active tracking.
6425 $installs = $this->get_blog_install_map();
6426 foreach ( $installs as $blog_id => $install ) {
6427 if ( $except_blog_id != $blog_id &&
6428 self::is_site_active( $blog_id ) &&
6429 $this->is_tracking_allowed( $blog_id, $install )
6430 ) {
6431 return $blog_id;
6432 }
6433 }
6434
6435 return 0;
6436 }
6437
6438 /**
6439 * @author Vova Feldman (@svovaf)
6440 * @since 2.0.0
6441 *
6442 * @param string $name Cron name.
6443 * @param string $action_tag Callback action tag.
6444 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6445 */
6446 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6447 $this->_logger->entrance( $name );
6448
6449 if ( ! $this->is_cron_on( $name ) ) {
6450 return;
6451 }
6452
6453 $clear_cron = true;
6454 if ( ! $is_network_clear && $this->_is_network_active ) {
6455 $installs = $this->get_blog_install_map();
6456
6457 foreach ( $installs as $blog_id => $install ) {
6458 /**
6459 * @var FS_Site $install
6460 */
6461 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6462 $clear_cron = false;
6463 break;
6464 }
6465 }
6466 }
6467
6468 if ( ! $clear_cron ) {
6469 return;
6470 }
6471
6472 $cron_blog_id = $this->get_cron_blog_id( $name );
6473
6474 $this->clear_cron_data( $name );
6475
6476 if ( 0 < $cron_blog_id ) {
6477 switch_to_blog( $cron_blog_id );
6478 }
6479
6480 if ( empty( $action_tag ) ) {
6481 $action_tag = $name;
6482 }
6483
6484 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6485
6486 if ( 0 < $cron_blog_id ) {
6487 restore_current_blog();
6488 }
6489 }
6490
6491 /**
6492 * Unix timestamp for next cron execution or false if not scheduled.
6493 *
6494 * @author Vova Feldman (@svovaf)
6495 * @since 2.0.0
6496 *
6497 * @param string $name Cron name.
6498 * @param string $action_tag Callback action tag.
6499 *
6500 * @return int|false
6501 */
6502 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6503 $this->_logger->entrance( $name );
6504
6505 if ( ! $this->is_cron_on( $name ) ) {
6506 return false;
6507 }
6508
6509 $cron_blog_id = $this->get_cron_blog_id( $name );
6510
6511 if ( 0 < $cron_blog_id ) {
6512 switch_to_blog( $cron_blog_id );
6513 }
6514
6515 if ( empty( $action_tag ) ) {
6516 $action_tag = $name;
6517 }
6518
6519 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6520
6521 if ( 0 < $cron_blog_id ) {
6522 restore_current_blog();
6523 }
6524
6525 return $next_scheduled;
6526 }
6527
6528 /**
6529 * @author Vova Feldman (@svovaf)
6530 * @since 2.0.0
6531 *
6532 * @param string $name Cron name.
6533 * @param string $action_tag Callback action tag.
6534 * @param string $recurrence 'single' or 'daily'.
6535 * @param int $start_at Defaults to now.
6536 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6537 * @param int $except_blog_id Target any except the excluded blog ID.
6538 */
6539 private function schedule_cron(
6540 $name,
6541 $action_tag = '',
6542 $recurrence = 'single',
6543 $start_at = WP_FS__SCRIPT_START_TIME,
6544 $randomize_start = true,
6545 $except_blog_id = 0
6546 ) {
6547 $this->_logger->entrance( $name );
6548
6549 $this->clear_cron( $name, $action_tag, true );
6550
6551 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6552
6553 if ( is_multisite() && 0 == $cron_blog_id ) {
6554 // Don't schedule cron since couldn't find a target blog.
6555 return;
6556 }
6557
6558 if ( 0 < $cron_blog_id ) {
6559 switch_to_blog( $cron_blog_id );
6560 }
6561
6562 if ( 'daily' === $recurrence ) {
6563 if ( $randomize_start ) {
6564 // Schedule first sync with a random 12 hour time range from now.
6565 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6566 }
6567
6568 // Schedule daily WP cron.
6569 wp_schedule_event(
6570 $start_at,
6571 'daily',
6572 $this->get_action_tag( $action_tag )
6573 );
6574 } else if ( 'single' === $recurrence ) {
6575 // Schedule single cron.
6576 wp_schedule_single_event(
6577 $start_at,
6578 $this->get_action_tag( $action_tag )
6579 );
6580 }
6581
6582 $this->set_cron_data( $name, $cron_blog_id );
6583
6584 if ( 0 < $cron_blog_id ) {
6585 restore_current_blog();
6586 }
6587 }
6588
6589 /**
6590 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6591 * that doesn't halt page loading.
6592 *
6593 * @author Vova Feldman (@svovaf)
6594 * @since 2.0.0
6595 *
6596 * @param string $name Cron name.
6597 * @param callable $callable The function that should be executed.
6598 */
6599 private function execute_cron( $name, $callable ) {
6600 $this->_logger->entrance( $name );
6601
6602 // Store the last time data sync was executed.
6603 $this->set_cron_execution_timestamp( $name );
6604
6605 // Check if API is temporary down.
6606 if ( FS_Api::is_temporary_down() ) {
6607 return;
6608 }
6609
6610 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6611
6612 $users_2_blog_ids = array();
6613
6614 if ( ! is_multisite() ) {
6615 // Add dummy blog.
6616 $users_2_blog_ids[0] = array( 0 );
6617 } else {
6618 $installs = $this->get_blog_install_map();
6619 foreach ( $installs as $blog_id => $install ) {
6620 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6621 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6622 $users_2_blog_ids[ $install->user_id ] = array();
6623 }
6624
6625 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6626 }
6627 }
6628 }
6629
6630 $current_blog_id = get_current_blog_id();
6631
6632 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6633 if ( 0 < $blog_ids[0] ) {
6634 $this->switch_to_blog( $blog_ids[0] );
6635 }
6636
6637 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6638
6639 foreach ( $blog_ids as $blog_id ) {
6640 $this->do_action( "after_{$name}_cron", $blog_id );
6641 }
6642 }
6643
6644 if ( is_multisite() ) {
6645 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6646
6647 $this->do_action( "after_{$name}_cron_multisite" );
6648 }
6649 }
6650
6651 #endregion
6652
6653 #----------------------------------------------------------------------------------
6654 #region Daily Sync Cron
6655 #----------------------------------------------------------------------------------
6656
6657
6658 /**
6659 * @author Vova Feldman (@svovaf)
6660 * @since 2.0.0
6661 *
6662 * @return bool
6663 */
6664 private function is_sync_cron_scheduled() {
6665 return $this->is_cron_on( 'sync' );
6666 }
6667
6668 /**
6669 * Get the sync cron's executing blog ID.
6670 *
6671 * @author Vova Feldman (@svovaf)
6672 * @since 2.0.0
6673 *
6674 * @return int
6675 */
6676 private function get_sync_cron_blog_id() {
6677 return $this->get_cron_blog_id( 'sync' );
6678 }
6679
6680 /**
6681 * @author Vova Feldman (@svovaf)
6682 * @since 1.1.7.3
6683 */
6684 private function run_manual_sync() {
6685 if ( ! $this->is_user_admin() ) {
6686 return;
6687 }
6688
6689 // Run manual sync.
6690 $this->_sync_cron();
6691
6692 // Reschedule next cron to run 24 hours from now (performance optimization).
6693 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6694 }
6695
6696 /**
6697 * Data sync cron job. Replaces the background sync non blocking HTTP request
6698 * that doesn't halt page loading.
6699 *
6700 * @author Vova Feldman (@svovaf)
6701 * @since 1.1.7.3
6702 * @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.
6703 */
6704 function _sync_cron() {
6705 $this->_logger->entrance();
6706
6707 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6708 }
6709
6710 /**
6711 * The actual data sync cron logic.
6712 *
6713 * @author Vova Feldman (@svovaf)
6714 * @since 2.0.0
6715 *
6716 * @param int[] $blog_ids
6717 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6718 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6719 * updates for a single site in case `execute_cron` has switched to a different blog.
6720 */
6721 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6722 if ( $this->is_registered() ) {
6723 if ( $this->has_paid_plan() ) {
6724 // Initiate background plan sync.
6725 $this->_sync_license( true, false, $current_blog_id );
6726
6727 if ( $this->is_paying() ) {
6728 // Check for premium plugin updates.
6729 $this->check_updates( true );
6730 }
6731 } else {
6732 // Sync install(s) (only if something changed locally).
6733 if ( 1 < count( $blog_ids ) ) {
6734 $this->sync_installs();
6735 } else {
6736 $this->sync_install();
6737 }
6738
6739 $this->maybe_sync_install_user();
6740 }
6741 }
6742 }
6743
6744 /**
6745 * Check if sync was executed in the last $period of seconds.
6746 *
6747 * @author Vova Feldman (@svovaf)
6748 * @since 1.1.7.3
6749 *
6750 * @param int $period In seconds
6751 *
6752 * @return bool
6753 */
6754 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6755 return $this->is_cron_executed( 'sync', $period );
6756 }
6757
6758 /**
6759 * @author Vova Feldman (@svovaf)
6760 * @since 1.1.7.3
6761 *
6762 * @return bool
6763 */
6764 private function is_sync_cron_on() {
6765 return $this->is_cron_on( 'sync' );
6766 }
6767
6768 /**
6769 * @author Leo Fajardo (@leorw)
6770 * @since 2.5.0
6771 */
6772 private function maybe_schedule_sync_cron() {
6773 $next_schedule = $this->next_sync_cron();
6774
6775 // The event is properly scheduled, so no need to reschedule it.
6776 if (
6777 is_numeric( $next_schedule ) &&
6778 $next_schedule > time()
6779 ) {
6780 return;
6781 }
6782
6783 $this->schedule_sync_cron();
6784 }
6785
6786 /**
6787 * @author Vova Feldman (@svovaf)
6788 * @since 1.1.7.3
6789 *
6790 * @param int $start_at Defaults to now.
6791 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6792 * @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.
6793 */
6794 private function schedule_sync_cron(
6795 $start_at = WP_FS__SCRIPT_START_TIME,
6796 $randomize_start = true,
6797 $except_blog_id = 0
6798 ) {
6799 $this->schedule_cron(
6800 'sync',
6801 'data_sync',
6802 'daily',
6803 $start_at,
6804 $randomize_start,
6805 $except_blog_id
6806 );
6807 }
6808
6809 /**
6810 * Add the actual sync function to the cron job hook.
6811 *
6812 * @author Vova Feldman (@svovaf)
6813 * @since 1.1.7.3
6814 */
6815 private function hook_callback_to_sync_cron() {
6816 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6817 }
6818
6819 /**
6820 * @author Vova Feldman (@svovaf)
6821 * @since 1.1.7.3
6822 *
6823 * @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.
6824 */
6825 private function clear_sync_cron( $is_network_clear = false ) {
6826 $this->_logger->entrance();
6827
6828 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6829 }
6830
6831 /**
6832 * Unix timestamp for next sync cron execution or false if not scheduled.
6833 *
6834 * @author Vova Feldman (@svovaf)
6835 * @since 1.1.7.3
6836 *
6837 * @return int|false
6838 */
6839 function next_sync_cron() {
6840 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6841 }
6842
6843 /**
6844 * Unix timestamp for previous sync cron execution or false if never executed.
6845 *
6846 * @author Vova Feldman (@svovaf)
6847 * @since 1.1.7.3
6848 *
6849 * @return int|false
6850 */
6851 function last_sync_cron() {
6852 return $this->cron_last_execution( 'sync' );
6853 }
6854
6855 #endregion Daily Sync Cron ------------------------------------------------------------------
6856
6857 #----------------------------------------------------------------------------------
6858 #region Async Install Sync
6859 #----------------------------------------------------------------------------------
6860
6861 /**
6862 * @author Vova Feldman (@svovaf)
6863 * @since 1.1.7.3
6864 *
6865 * @return bool
6866 */
6867 private function is_install_sync_scheduled() {
6868 return $this->is_cron_on( 'install_sync' );
6869 }
6870
6871 /**
6872 * Get the sync cron's executing blog ID.
6873 *
6874 * @author Vova Feldman (@svovaf)
6875 * @since 2.0.0
6876 *
6877 * @return int
6878 */
6879 private function get_install_sync_cron_blog_id() {
6880 return $this->get_cron_blog_id( 'install_sync' );
6881 }
6882
6883 /**
6884 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6885 *
6886 * @author Vova Feldman (@svovaf)
6887 * @since 1.1.7.3
6888 *
6889 * @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.
6890 */
6891 private function schedule_install_sync( $except_blog_id = 0 ) {
6892 if ( $this->is_clone() ) {
6893 return;
6894 }
6895
6896 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6897 }
6898
6899 /**
6900 * Unix timestamp for previous install sync cron execution or false if never executed.
6901 *
6902 * @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.
6903 *
6904 * @author Vova Feldman (@svovaf)
6905 * @since 1.1.7.3
6906 *
6907 * @return int|false
6908 */
6909 function last_install_sync() {
6910 return $this->cron_last_execution( 'install_sync' );
6911 }
6912
6913 /**
6914 * Unix timestamp for next install sync cron execution or false if not scheduled.
6915 *
6916 * @author Vova Feldman (@svovaf)
6917 * @since 1.1.7.3
6918 *
6919 * @return int|false
6920 */
6921 function next_install_sync() {
6922 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6923 }
6924
6925 /**
6926 * Add the actual install sync function to the cron job hook.
6927 *
6928 * @author Vova Feldman (@svovaf)
6929 * @since 1.1.7.3
6930 */
6931 private function hook_callback_to_install_sync() {
6932 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6933 }
6934
6935 /**
6936 * @author Vova Feldman (@svovaf)
6937 * @since 1.1.7.3
6938 *
6939 * @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.
6940 */
6941 private function clear_install_sync_cron( $is_network_clear = false ) {
6942 $this->_logger->entrance();
6943
6944 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6945 }
6946
6947 /**
6948 * @author Vova Feldman (@svovaf)
6949 * @since 1.1.7.3
6950 * @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.
6951 */
6952 public function _run_sync_install() {
6953 $this->_logger->entrance();
6954
6955 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6956 }
6957
6958 /**
6959 * The actual install(s) sync cron logic.
6960 *
6961 * @author Vova Feldman (@svovaf)
6962 * @since 2.0.0
6963 *
6964 * @param int[] $blog_ids
6965 * @param int|null $current_blog_id
6966 */
6967 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6968 if ( $this->is_registered() ) {
6969 if ( 1 < count( $blog_ids ) ) {
6970 $this->sync_installs( array(), true );
6971 } else {
6972 $this->sync_install( array(), true );
6973 }
6974
6975 $this->maybe_sync_install_user();
6976 }
6977 }
6978
6979 #endregion Async Install Sync ------------------------------------------------------------------
6980
6981 /**
6982 * Show a notice that activation is currently pending.
6983 *
6984 * @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.
6985 *
6986 * @author Vova Feldman (@svovaf)
6987 * @since 1.0.7
6988 *
6989 * @param bool|string $email_address
6990 * @param bool $is_pending_trial Since 1.2.1.5
6991 * @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.
6992 * @param bool $has_upgrade_context Since 2.5.3
6993 * @param bool $support_email_address Since 2.5.3
6994 */
6995 function _add_pending_activation_notice(
6996 $email_address = false,
6997 $is_pending_trial = false,
6998 $is_suspicious_email = false,
6999 $has_upgrade_context = false,
7000 $support_email_address = false
7001 ) {
7002 if ( ! is_string( $email_address ) ) {
7003 $current_user = self::_get_current_wp_user();
7004 $email_address = $current_user->user_email;
7005 }
7006
7007 $formatted_message_args = array(
7008 "<b>{$this->get_plugin_name()}</b>",
7009 "<b>{$email_address}</b>",
7010 );
7011
7012 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
7013 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
7014 $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' );
7015
7016 $formatted_message_args[] = $is_pending_trial ?
7017 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7018 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
7019
7020 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
7021 } else {
7022 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
7023 $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.' );
7024
7025 if ( $this->has_release_on_freemius() ) {
7026 $formatted_message_args[] = $this->get_text_x_inline(
7027 'the installation instructions',
7028 'Part of the message telling the user what they should receive via email.',
7029 'the-installation-instructions-phrase'
7030 );
7031 } else {
7032 $formatted_message_args[] = $this->get_text_x_inline(
7033 'a license key',
7034 'Part of the message telling the user what they should receive via email.',
7035 'a-license-key-phrase'
7036 );
7037
7038 $formatted_message .= ( ' ' . sprintf(
7039 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
7040 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
7041 sprintf(
7042 '<b><a href="%s">%s</a></b>',
7043 $this->get_activation_url( array(
7044 'fs_action' => 'reset_pending_activation_mode',
7045 'require_license' => 'true',
7046 'fs_unique_affix' => $this->get_unique_affix(),
7047 ) ),
7048 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
7049 )
7050 ) );
7051 }
7052
7053 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
7054 ( "<b>{$support_email_address}</b>" ) :
7055 $this->get_text_x_inline(
7056 "the product's support email address",
7057 'Part of the message that tells the user to check their spam folder for a specific email.',
7058 'product-support-email-address-phrase'
7059 );
7060
7061 $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' ) );
7062
7063 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
7064 }
7065
7066 $this->_admin_notices->add_sticky(
7067 vsprintf( $formatted_message, $formatted_message_args ),
7068 'activation_pending',
7069 $notice_title
7070 );
7071 }
7072
7073 /**
7074 * Check if currently in plugin activation.
7075 *
7076 * @author Vova Feldman (@svovaf)
7077 * @since 1.1.4
7078 *
7079 * @return bool
7080 */
7081 function is_plugin_activation() {
7082 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7083
7084 return !empty($result);
7085 }
7086
7087 /**
7088 *
7089 * NOTE: admin_menu action executed before admin_init.
7090 *
7091 * @author Vova Feldman (@svovaf)
7092 * @since 1.0.7
7093 */
7094 function _admin_init_action() {
7095 $is_migration = $this->is_migration();
7096
7097 /**
7098 * Automatically redirect to connect/activation page after plugin activation.
7099 *
7100 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7101 */
7102 if ( $this->is_plugin_activation() ) {
7103 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7104
7105 if ( isset( $_GET['activate-multi'] ) ) {
7106 /**
7107 * Don't redirect if activating multiple plugins at once (bulk activation).
7108 */
7109 } else if (
7110 self::is_deactivation_snoozed() &&
7111 (
7112 // Either running the free code base.
7113 ! $this->is_premium() ||
7114 // Or if has a free version.
7115 ! $this->is_only_premium() ||
7116 // If premium only, don't redirect if license is activated.
7117 ( $this->is_registered() && ! $this->can_use_premium_code() )
7118 )
7119 ) {
7120 /**
7121 * 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.
7122 */
7123 } else if ( ! $is_migration ) {
7124 $this->_redirect_on_activation_hook();
7125 return;
7126 }
7127 }
7128
7129 if ( $is_migration ) {
7130 return;
7131 }
7132
7133 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7134 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7135
7136 $this->skip_connection( fs_is_network_admin() );
7137
7138 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7139 }
7140
7141 if ( $this->is_network_activation_mode() &&
7142 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7143 ) {
7144 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7145
7146 $this->delegate_connection();
7147
7148 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7149 }
7150
7151 $this->_add_upgrade_action_link();
7152
7153 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7154 (
7155 ( true === $this->_storage->require_license_activation ) ||
7156 // Not registered nor anonymous.
7157 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7158 // OR, network level and in network upgrade mode.
7159 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7160 )
7161 ) {
7162 if ( ! $this->is_pending_activation() ) {
7163 if ( ! $this->is_activation_page() ) {
7164 /**
7165 * If a user visits any other admin page before activating the premium-only theme with a valid
7166 * license, reactivate the previous theme.
7167 *
7168 * @author Leo Fajardo (@leorw)
7169 * @since 1.2.2
7170 */
7171 if ( $this->is_theme() &&
7172 ! $this->has_settings_menu() &&
7173 ! isset( $_REQUEST['fs_action'] ) &&
7174 $this->can_activate_previous_theme()
7175 ) {
7176 if ( $this->is_only_premium() ) {
7177 $this->activate_previous_theme();
7178 return;
7179 }
7180
7181 if ( true === $this->_storage->require_license_activation ) {
7182 $this->_storage->require_license_activation = false;
7183 }
7184 }
7185
7186 if ( ! fs_is_network_admin() &&
7187 $this->is_network_activation_mode() &&
7188 ! $this->is_delegated_connection()
7189 ) {
7190 return;
7191 }
7192
7193 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7194 if ( ! $this->_anonymous_mode &&
7195 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7196 // Show notice for new plugin installations.
7197 $this->_admin_notices->add(
7198 sprintf(
7199 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7200 sprintf( '<b><a href="%s">%s</a></b>',
7201 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7202 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7203 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7204 )
7205 ),
7206 '',
7207 'update-nag'
7208 );
7209 }
7210 } else {
7211 if ( $this->should_add_sticky_optin_notice() ) {
7212 $this->add_sticky_optin_admin_notice();
7213 }
7214
7215 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7216 // Don't show admin nag if plugin update.
7217 wp_enqueue_script( 'wp-pointer' );
7218 wp_enqueue_style( 'wp-pointer' );
7219
7220 $this->_enqueue_connect_essentials();
7221
7222 add_action( 'admin_print_footer_scripts', array(
7223 $this,
7224 '_add_connect_pointer_script'
7225 ) );
7226 }
7227 }
7228 }
7229 }
7230
7231 if ( $this->show_opt_in_on_themes_page() &&
7232 $this->is_activation_page()
7233 ) {
7234 $this->_show_theme_activation_optin_dialog();
7235 }
7236 }
7237 }
7238
7239 /**
7240 * @author Vova Feldman (@svovaf)
7241 * @since 2.0.0
7242 *
7243 * @return bool
7244 */
7245 private function should_add_sticky_optin_notice() {
7246 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7247 return false;
7248 }
7249
7250 if ( fs_is_network_admin() ) {
7251 if ( ! $this->_is_network_active ) {
7252 return false;
7253 }
7254
7255 if ( ! $this->is_network_activation_mode() ) {
7256 return false;
7257 }
7258
7259 return ! isset( $this->_storage->sticky_optin_added_ms );
7260 }
7261
7262 if ( ! $this->is_activation_mode() ) {
7263 return false;
7264 }
7265
7266 // If running from a blog admin and delegated the connection.
7267 return ! isset( $this->_storage->sticky_optin_added );
7268 }
7269
7270 /**
7271 * @author Leo Fajardo (@leorw)
7272 * @since 2.0.0
7273 */
7274 private function add_sticky_optin_admin_notice() {
7275 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7276 $this->_storage->sticky_optin_added = true;
7277 } else {
7278 $this->_storage->sticky_optin_added_ms = true;
7279 }
7280
7281 // Show notice for new plugin installations.
7282 $this->_admin_notices->add_sticky(
7283 sprintf(
7284 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7285 $this->_module_type,
7286 sprintf( '<b><a href="%s">%s</a></b>',
7287 $this->get_activation_url(),
7288 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7289 )
7290 ),
7291 'connect_account',
7292 '',
7293 'update-nag'
7294 );
7295 }
7296
7297 /**
7298 * Enqueue connect requires scripts and styles.
7299 *
7300 * @author Vova Feldman (@svovaf)
7301 * @since 1.1.4
7302 */
7303 function _enqueue_connect_essentials() {
7304 wp_enqueue_script( 'jquery' );
7305 wp_enqueue_script( 'json2' );
7306
7307 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7308 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7309 }
7310
7311 /**
7312 * Add connect / opt-in pointer.
7313 *
7314 * @author Vova Feldman (@svovaf)
7315 * @since 1.1.4
7316 */
7317 function _add_connect_pointer_script() {
7318 $vars = array( 'id' => $this->_module_id );
7319 $pointer_content = fs_get_template( 'connect.php', $vars );
7320 ?>
7321 <script type="text/javascript">// <![CDATA[
7322 jQuery(document).ready(function ($) {
7323 if ('undefined' !== typeof(jQuery().pointer)) {
7324
7325 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7326
7327 if (element.length > 0) {
7328 var optin = $(element).pointer($.extend(true, {}, {
7329 content : <?php echo json_encode( $pointer_content ) ?>,
7330 position : {
7331 edge : 'left',
7332 align: 'center'
7333 },
7334 buttons : function () {
7335 // Don't show pointer buttons.
7336 return '';
7337 },
7338 pointerWidth: 482
7339 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7340
7341 <?php
7342 echo $this->apply_filters( 'optin_pointer_execute', "
7343
7344 optin.pointer('open');
7345
7346 // Tag the opt-in pointer with custom class.
7347 $('.wp-pointer #fs_connect')
7348 .parents('.wp-pointer.wp-pointer-top')
7349 .addClass('fs-opt-in-pointer');
7350
7351 ", 'element', 'optin' ) ?>
7352 }
7353 }
7354 });
7355 // ]]></script>
7356 <?php
7357 }
7358
7359 /**
7360 * Return current page's URL.
7361 *
7362 * @author Vova Feldman (@svovaf)
7363 * @since 1.0.7
7364 *
7365 * @return string
7366 */
7367 static function current_page_url() {
7368 $url = 'http';
7369
7370 if ( isset( $_SERVER["HTTPS"] ) ) {
7371 if ( $_SERVER["HTTPS"] == "on" ) {
7372 $url .= "s";
7373 }
7374 }
7375 $url .= "://";
7376 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7377 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7378 } else {
7379 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7380 }
7381
7382 return esc_url( $url );
7383 }
7384
7385 /**
7386 * Check if the current page is the plugin's main admin settings page.
7387 *
7388 * @author Vova Feldman (@svovaf)
7389 * @since 1.0.7
7390 *
7391 * @return bool
7392 */
7393 function _is_plugin_page() {
7394 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7395 fs_is_plugin_page( $this->_slug );
7396 }
7397
7398 /* Events
7399 ------------------------------------------------------------------------------------------------------------------*/
7400 /**
7401 * Delete site install from Database.
7402 *
7403 * @author Vova Feldman (@svovaf)
7404 * @since 1.0.1
7405 *
7406 * @param bool $store
7407 * @param int|null $blog_id Since 2.0.0
7408 *
7409 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7410 */
7411 function _delete_site( $store = true, $blog_id = null ) {
7412 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7413 }
7414
7415 /**
7416 * Delete site install from Database.
7417 *
7418 * @author Vova Feldman (@svovaf)
7419 * @since 1.2.2.7
7420 *
7421 * @param string $slug
7422 * @param string $module_type
7423 * @param bool $store
7424 * @param int|null $blog_id Since 2.0.0
7425 *
7426 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7427 */
7428 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7429 $sites = self::get_all_sites( $module_type, $blog_id );
7430
7431 $install_id = false;
7432
7433 if ( isset( $sites[ $slug ] ) ) {
7434 if ( is_object( $sites[ $slug ] ) ) {
7435 $install_id = $sites[ $slug ]->id;
7436 }
7437
7438 unset( $sites[ $slug ] );
7439
7440 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7441 }
7442
7443 return $install_id;
7444 }
7445
7446 /**
7447 * Delete user.
7448 *
7449 * @author Vova Feldman (@svovaf)
7450 * @since 2.0.0
7451 *
7452 * @param number $user_id
7453 * @param bool $store
7454 *
7455 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7456 */
7457 private static function delete_user( $user_id, $store = true ) {
7458 $users = self::get_all_users();
7459
7460 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7461 return false;
7462 }
7463
7464 unset( $users[ $user_id ] );
7465
7466 self::$_accounts->set_option( 'users', $users, $store );
7467
7468 return $user_id;
7469 }
7470
7471 /**
7472 * Delete plugin's plans information.
7473 *
7474 * @param bool $store Flush to Database if true.
7475 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7476 *
7477 * @author Vova Feldman (@svovaf)
7478 * @since 1.0.9
7479 */
7480 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7481 $this->_logger->entrance();
7482
7483 $plans = self::get_all_plans( $this->_module_type );
7484
7485 $plans_to_keep = array();
7486
7487 if ( $keep_associated_plans ) {
7488 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7489 foreach ( $plans_ids_to_keep as $plan_id ) {
7490 $plan = self::_get_plan_by_id( $plan_id );
7491 if ( is_object( $plan ) ) {
7492 $plans_to_keep[] = self::_encrypt_entity( $plan );
7493 }
7494 }
7495 }
7496
7497 if ( ! empty( $plans_to_keep ) ) {
7498 $plans[ $this->_slug ] = $plans_to_keep;
7499 } else {
7500 unset( $plans[ $this->_slug ] );
7501 }
7502
7503 $this->set_account_option( 'plans', $plans, $store );
7504 }
7505
7506 /**
7507 * Delete all plugin licenses.
7508 *
7509 * @author Vova Feldman (@svovaf)
7510 * @since 1.0.9
7511 *
7512 * @param bool $store
7513 */
7514 private function _delete_licenses( $store = true ) {
7515 $this->_logger->entrance();
7516
7517 $all_licenses = self::get_all_licenses();
7518
7519 unset( $all_licenses[ $this->_module_id ] );
7520
7521 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7522 }
7523
7524 /**
7525 * Check if Freemius was added on new plugin installation.
7526 *
7527 * @author Vova Feldman (@svovaf)
7528 * @since 1.1.5
7529 *
7530 * @return bool
7531 */
7532 function is_plugin_new_install() {
7533 return isset( $this->_storage->is_plugin_new_install ) &&
7534 $this->_storage->is_plugin_new_install;
7535 }
7536
7537 /**
7538 * Check if it's the first plugin release that is running Freemius.
7539 *
7540 * @author Vova Feldman (@svovaf)
7541 * @since 1.2.1.5
7542 *
7543 * @return bool
7544 */
7545 function is_first_freemius_powered_version() {
7546 return empty( $this->_storage->plugin_last_version );
7547 }
7548
7549 /**
7550 * @author Leo Fajardo (@leorw)
7551 * @since 1.2.2
7552 *
7553 * @return bool|string
7554 */
7555 private function get_previous_theme_slug() {
7556 return isset( $this->_storage->previous_theme ) ?
7557 $this->_storage->previous_theme :
7558 false;
7559 }
7560
7561 /**
7562 * @author Leo Fajardo (@leorw)
7563 * @since 1.2.2
7564 *
7565 * @return bool
7566 */
7567 private function can_activate_previous_theme() {
7568 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7569 }
7570
7571 /**
7572 * @author Leo Fajardo (@leorw)
7573 * @since 2.5.0
7574 *
7575 * @return bool
7576 */
7577 private function can_activate_theme( $slug ) {
7578 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7579 $theme_instance = wp_get_theme( $slug );
7580
7581 return $theme_instance->exists();
7582 }
7583
7584 return false;
7585 }
7586
7587 /**
7588 * @author Leo Fajardo (@leorw)
7589 * @since 1.2.2
7590 */
7591 private function activate_previous_theme() {
7592 switch_theme( $this->get_previous_theme_slug() );
7593 unset( $this->_storage->previous_theme );
7594
7595 global $pagenow;
7596 if ( 'themes.php' === $pagenow ) {
7597 /**
7598 * Refresh the active theme information.
7599 *
7600 * @author Leo Fajardo (@leorw)
7601 * @since 1.2.2
7602 */
7603 fs_redirect( $this->admin_url( $pagenow ) );
7604 }
7605 }
7606
7607 /**
7608 * @author Leo Fajardo (@leorw)
7609 * @since 1.2.2
7610 *
7611 * @return string
7612 */
7613 function get_previous_theme_activation_url() {
7614 if ( ! $this->can_activate_previous_theme() ) {
7615 return '';
7616 }
7617
7618 /**
7619 * Activation URL
7620 *
7621 * @author Leo Fajardo (@leorw)
7622 * @since 1.2.2
7623 */
7624 return wp_nonce_url(
7625 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7626 'switch-theme_' . $this->get_previous_theme_slug()
7627 );
7628 }
7629
7630 /**
7631 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7632 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7633 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7634 * theme doesn't exist, then there will be no close button.
7635 *
7636 * @author Leo Fajardo (@leorw)
7637 * @since 1.2.2
7638 *
7639 * @param string $slug_or_name Old theme's slug or name.
7640 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7641 */
7642 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7643 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7644 $old_theme->get_stylesheet() :
7645 $slug_or_name;
7646
7647 $this->_activate_plugin_event_hook();
7648 }
7649
7650 /**
7651 * Plugin activated hook.
7652 *
7653 * @author Vova Feldman (@svovaf)
7654 * @since 1.0.1
7655 *
7656 * @uses FS_Api
7657 */
7658 function _activate_plugin_event_hook() {
7659 $this->_logger->entrance( 'slug = ' . $this->_slug );
7660
7661 if ( ! $this->is_user_admin() ) {
7662 return;
7663 }
7664
7665 $this->unregister_uninstall_hook();
7666
7667 // Clear API cache on activation.
7668 FS_Api::clear_cache();
7669
7670 $is_premium_version_activation = $this->is_plugin() ?
7671 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7672 $this->is_premium();
7673
7674 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7675 $this->clear_pending_activation_mode();
7676 }
7677
7678 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7679
7680 if ( $this->is_plugin() ) {
7681 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7682 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7683 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7684 $other_version_basename = $is_premium_version_activation ?
7685 $this->_free_plugin_basename :
7686 $this->premium_plugin_basename();
7687
7688 if ( ! $this->_is_network_active ) {
7689 /**
7690 * Themes are always network activated, but the ACTUAL activation is per site.
7691 *
7692 * During the activation, the plugin isn't yet active, therefore,
7693 * _is_network_active will be set to false even if it's a network level
7694 * activation. So we need to fix that by looking at the is_network_admin() value.
7695 *
7696 * @author Vova Feldman
7697 */
7698 $this->_is_network_active = (
7699 $this->_is_multisite_integrated &&
7700 fs_is_network_admin()
7701 );
7702 }
7703
7704 /**
7705 * If the other module version is active, deactivate it.
7706 *
7707 * is_plugin_active() checks if the plugin is active on the site or the network level and
7708 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7709 *
7710 * @author Leo Fajardo (@leorw)
7711 * @since 1.2.2
7712 */
7713 if (
7714 is_plugin_active( $other_version_basename ) &&
7715 $this->apply_filters( 'deactivate_on_activation', true )
7716 ) {
7717 deactivate_plugins( $other_version_basename );
7718 }
7719 }
7720
7721 if ( $this->is_registered() ) {
7722 if ( $is_premium_version_activation ) {
7723 $this->reconnect_locally();
7724 }
7725
7726
7727 // Schedule re-activation event and sync.
7728 // $this->sync_install( array(), true );
7729 $this->schedule_install_sync();
7730
7731 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7732 if ( $is_premium_version_activation ) {
7733 $this->_admin_notices->add(
7734 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7735 $this->get_text_x_inline( 'W00t',
7736 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7737 );
7738 }
7739 } else if ( $this->is_anonymous() ) {
7740 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7741 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7742 $network = true;
7743 } else {
7744 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7745 $this->_storage->is_anonymous['version'] :
7746 null;
7747 $network = false;
7748 }
7749
7750 /**
7751 * Reset "skipped" click cache on the following:
7752 * 1. Freemius DEV mode.
7753 * 2. WordPress DEBUG mode.
7754 * 3. If a plugin and the user skipped the exact same version before.
7755 *
7756 * @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).
7757 *
7758 * @todo 4. If explicitly asked to retry after every activation.
7759 */
7760 if ( WP_FS__DEV_MODE ||
7761 (
7762 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7763 $this->get_plugin_version() == $plugin_version
7764 )
7765 ) {
7766 $this->reset_anonymous_mode( $network );
7767 }
7768 }
7769
7770 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7771
7772 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7773 /**
7774 * When activating an add-on, try to also activate a license.
7775 *
7776 * @author Leo Fajardo (@leorw)
7777 * @since 2.3.0
7778 */
7779 if ( ! $this->_is_network_active ) {
7780 $this->maybe_activate_addon_license();
7781 } else {
7782 $this->maybe_network_activate_addon_license();
7783 }
7784
7785 /**
7786 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7787 *
7788 * @author Leo Fajardo (@leorw)
7789 * @since 2.3.0
7790 */
7791 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7792
7793 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7794 $this->_storage->require_license_activation = false;
7795 }
7796 }
7797
7798 if (
7799 $is_premium_version_activation &&
7800 (
7801 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7802 (
7803 $this->is_registered() &&
7804 ! $is_trial_or_has_features_enabled_license
7805 )
7806 )
7807 ) {
7808 $this->_storage->require_license_activation = true;
7809 }
7810
7811 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7812 /**
7813 * If no previous version of plugin's version exist, it means that it's either
7814 * the first time that the plugin installed on the site, or the plugin was installed
7815 * before but didn't have Freemius integrated.
7816 *
7817 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7818 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7819 * only after immediate activation.
7820 *
7821 * @since 1.1.4
7822 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7823 */
7824 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7825 }
7826
7827 /**
7828 * Also flush when activating the premium version so that even if Freemius was off before, the API
7829 * connectivity test can be run again.
7830 *
7831 * @author Leo Fajardo (@leorw)
7832 * @since 2.2.3.1
7833 */
7834 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7835
7836 if ( ! $this->_anonymous_mode &&
7837 ( false !== $has_api_connectivity ) &&
7838 ! $this->_isAutoInstall
7839 ) {
7840 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7841 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7842 }
7843
7844 /**
7845 * Activation hook is executed after the plugin's main file is loaded, therefore,
7846 * after the plugin was loaded. The logic is located at activate_plugin()
7847 * ./wp-admin/includes/plugin.php.
7848 *
7849 * @author Vova Feldman (@svovaf)
7850 * @since 1.1.9
7851 */
7852 $this->_storage->was_plugin_loaded = true;
7853 }
7854
7855 /**
7856 * @author Leo Fajardo (@leorw)
7857 * @since 2.3.0
7858 */
7859 private function maybe_activate_addon_license() {
7860 $parent_fs = $this->get_parent_instance();
7861
7862 if (
7863 ! is_object( $parent_fs ) ||
7864 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7865 ) {
7866 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7867 return;
7868 }
7869
7870 $license = $this->get_active_parent_license();
7871 if ( ! is_object( $license ) ) {
7872 return;
7873 }
7874
7875 if (
7876 $this->is_bundle_license_auto_activation_enabled() &&
7877 ! empty( $license->products )
7878 ) {
7879 $this->activate_bundle_license( $license );
7880
7881 return;
7882 }
7883
7884 if ( ! $this->is_registered() ) {
7885 // Opt in with a license key.
7886 $this->opt_in(
7887 $parent_fs->get_current_or_network_user()->email,
7888 false,
7889 false,
7890 $license->secret_key
7891 );
7892 } else {
7893 // Activate the license.
7894 $install = $this->api_site_call(
7895 '/',
7896 'put',
7897 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7898 );
7899
7900 if ( ! FS_Api::is_api_error( $install ) ) {
7901 $this->_sync_addon_license( $this->get_id(), true );
7902 }
7903 }
7904 }
7905
7906 /**
7907 * @author Leo Fajardo (@leorw)
7908 * @since 2.3.0
7909 *
7910 * @param FS_Plugin_License $license
7911 */
7912 private function maybe_network_activate_addon_license( $license = null ) {
7913 $parent_fs = $this->get_parent_instance();
7914 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7915 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7916 return;
7917 }
7918
7919 $license = ( ! is_null( $license ) ) ?
7920 $license :
7921 $this->get_active_parent_license();
7922
7923 if ( ! is_object( $license ) ) {
7924 return;
7925 }
7926
7927 if (
7928 $this->is_bundle_license_auto_activation_enabled() &&
7929 ! empty( $license->products )
7930 ) {
7931 $this->activate_bundle_license( $license );
7932
7933 return;
7934 }
7935
7936 if ( ! $this->is_network_registered() ) {
7937 $sites = $this->get_sites_for_network_level_optin();
7938
7939 if ( count( $sites ) > $license->left() ) {
7940 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7941 return;
7942 }
7943
7944 // Opt in with a license key.
7945 $this->opt_in(
7946 $parent_fs->get_user()->email,
7947 false,
7948 false,
7949 $license->secret_key,
7950 false,
7951 false,
7952 false,
7953 null,
7954 $sites
7955 );
7956 } else {
7957 $blog_2_install_map = array();
7958 $site_ids = array();
7959
7960 $all_sites = Freemius::get_sites();
7961
7962 foreach ( $all_sites as $site ) {
7963 $blog_id = Freemius::get_site_blog_id( $site );
7964 $install = $this->get_install_by_blog_id( $blog_id );
7965
7966 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7967 // Skip license activation for installs that are already associated with a license.
7968 continue;
7969 }
7970
7971 if ( is_object( $install ) ) {
7972 $blog_2_install_map[ $blog_id ] = $install;
7973 } else {
7974 $site_ids[] = $blog_id;
7975 }
7976 }
7977
7978 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7979 return;
7980 }
7981
7982 $user = $this->get_current_or_network_user();
7983
7984 if ( ! empty( $blog_2_install_map ) ) {
7985 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7986
7987 if ( true !== $result ) {
7988 return;
7989 }
7990 }
7991
7992 if ( ! empty( $site_ids ) ) {
7993 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7994 }
7995 }
7996 }
7997
7998 /**
7999 * 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.
8000 *
8001 * @author Leo Fajardo (@leorw)
8002 * @since 2.4.0
8003 *
8004 * @param FS_Plugin_License $license
8005 * @param array $sites
8006 * @param int $blog_id
8007 */
8008 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8009 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8010 $license = $this->_license;
8011 }
8012
8013 if ( ! is_object( $license ) ) {
8014 return;
8015 }
8016
8017 $parent_license = ( ! empty( $license->products ) ) ?
8018 $license :
8019 $this->get_active_parent_license( $license->secret_key );
8020
8021 if ( is_object( $parent_license ) ) {
8022 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8023 }
8024 }
8025
8026 /**
8027 * Try to activate a bundle license for all the bundle products installed on the site.
8028 * (1) If a child product install already has a license, the bundle license won't be activated.
8029 * (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.
8030 * (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.
8031 *
8032 * @author Leo Fajardo (@leorw)
8033 * @since 2.4.0
8034 *
8035 * @param FS_Plugin_License $license
8036 * @param array $sites
8037 * @param int $current_blog_id
8038 */
8039 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8040 $is_network_admin = fs_is_network_admin();
8041
8042 $installs_by_blog_map = array();
8043 $site_info_by_blog_map = array();
8044
8045 /**
8046 * Try to activate the license for all supported products.
8047 *
8048 * @author Leo Fajardo
8049 */
8050 foreach ( $license->products as $product_id ) {
8051 $fs = self::get_instance_by_id( $product_id );
8052
8053 if ( ! is_object( $fs ) ) {
8054 continue;
8055 }
8056
8057 if ( ! $fs->has_paid_plan() ) {
8058 continue;
8059 }
8060
8061 if (
8062 ! $fs->is_addon() &&
8063 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8064 ) {
8065 /**
8066 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8067 * there is a context bundle.
8068 */
8069 continue;
8070 }
8071
8072 if ( $current_blog_id > 0 ) {
8073 $fs->switch_to_blog( $current_blog_id );
8074 }
8075
8076 if ( $fs->has_active_valid_license() ) {
8077 continue;
8078 }
8079
8080 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8081 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8082 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8083 continue;
8084 }
8085 } else {
8086 if ( ! $fs->is_network_active() ) {
8087 // Do not try to activate the license in the network level if the product is not network active.
8088 continue;
8089 }
8090
8091 if ( $fs->is_network_delegated_connection() ) {
8092 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8093 continue;
8094 }
8095
8096 $has_install_with_license = false;
8097
8098 // 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.
8099 $filtered_sites = array();
8100
8101 if ( empty( $sites ) ) {
8102 $all_sites = self::get_sites();
8103
8104 foreach ( $all_sites as $site ) {
8105 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8106 }
8107 } else {
8108 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8109 foreach ( $sites as $site ) {
8110 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8111 continue;
8112 }
8113
8114 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8115 }
8116 }
8117
8118 foreach ( $sites as $site ) {
8119 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8120 continue;
8121 }
8122
8123 $blog_id = $site['blog_id'];
8124
8125 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8126 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8127 }
8128
8129 $installs = $installs_by_blog_map[ $blog_id ];
8130 $install = null;
8131
8132 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8133 $install = $installs[ $fs->get_slug() ];
8134
8135 if (
8136 is_object( $install ) &&
8137 (
8138 ! FS_Site::is_valid_id( $install->id ) ||
8139 ! FS_User::is_valid_id( $install->user_id ) ||
8140 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8141 )
8142 ) {
8143 $install = null;
8144 }
8145 }
8146
8147 if (
8148 is_object( $install ) &&
8149 FS_Plugin_License::is_valid_id( $install->license_id )
8150 ) {
8151 $has_install_with_license = true;
8152 break;
8153 }
8154
8155 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8156 // Site activation delegated, don't activate bundle license on the site in the network admin.
8157 continue;
8158 }
8159
8160 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8161 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8162 }
8163
8164 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8165 }
8166
8167 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8168 // 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.
8169 continue;
8170 }
8171
8172 $sites = $filtered_sites;
8173 }
8174
8175 $fs->activate_migrated_license(
8176 $license->secret_key,
8177 null,
8178 null,
8179 $sites,
8180 ( $current_blog_id > 0 ? $current_blog_id : null )
8181 );
8182 }
8183 }
8184
8185 /**
8186 * Returns a parent license that can be activated for the context product.
8187 *
8188 * @author Leo Fajardo (@leorw)
8189 * @since 2.3.0
8190 *
8191 * @param string|null $license_key
8192 * @param bool $flush
8193 *
8194 * @return FS_Plugin_License
8195 */
8196 function get_active_parent_license( $license_key = null, $flush = true ) {
8197 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8198
8199 $fs = $this;
8200
8201 if ( $this->is_addon() ) {
8202 $parent_instance = $this->get_parent_instance();
8203
8204 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8205 $fs = $parent_instance;
8206 }
8207 }
8208
8209 $foreign_licenses = $fs->get_foreign_licenses_info(
8210 self::get_all_licenses( $this->get_parent_id() )
8211 );
8212
8213 if ( ! empty ( $foreign_licenses ) ) {
8214 $foreign_licenses = array(
8215 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8216 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8217 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8218 );
8219
8220 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8221 }
8222
8223 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8224
8225 if (
8226 ! $this->is_api_result_object( $result, 'licenses' ) ||
8227 ! is_array( $result->licenses ) ||
8228 empty( $result->licenses )
8229 ) {
8230 return null;
8231 }
8232
8233 $parent_license = null;
8234
8235 if ( empty( $license_key ) ) {
8236 $parent_license = $result->licenses[0];
8237 } else {
8238 foreach ( $result->licenses as $license ) {
8239 if ( $license_key === $license->secret_key ) {
8240 $parent_license = $license;
8241 break;
8242 }
8243 }
8244 }
8245
8246 if ( ! is_null( $parent_license ) ) {
8247 $parent_license = new FS_Plugin_License( $parent_license );
8248 }
8249
8250 return $parent_license;
8251 }
8252
8253 /**
8254 * @author Leo Fajardo (@leorw)
8255 * @since 2.3.0
8256 *
8257 * @return array
8258 */
8259 function get_sites_for_network_level_optin() {
8260 $sites = array();
8261 $all_sites = self::get_sites();
8262
8263 foreach ( $all_sites as $site ) {
8264 $blog_id = self::get_site_blog_id( $site );
8265
8266 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8267 ! $this->is_installed_on_site( $blog_id )
8268 ) {
8269 $sites[] = $this->get_site_info( $site );
8270 }
8271 }
8272
8273 return $sites;
8274 }
8275
8276 /**
8277 * Delete account.
8278 *
8279 * @author Vova Feldman (@svovaf)
8280 * @since 1.0.3
8281 *
8282 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8283 */
8284 function delete_account_event( $check_user = true ) {
8285 $this->_logger->entrance( 'slug = ' . $this->_slug );
8286
8287 if ( $check_user && ! $this->is_user_admin() ) {
8288 return;
8289 }
8290
8291 $this->do_action( 'before_account_delete' );
8292
8293 // Clear all admin notices.
8294 $this->_admin_notices->clear_all_sticky( false );
8295
8296 $this->_delete_site( false );
8297
8298 $delete_network_common_data = true;
8299
8300 if ( $this->_is_network_active ) {
8301 $installs = $this->get_blog_install_map();
8302
8303 // Don't delete common network data unless no other installs left.
8304 $delete_network_common_data = empty( $installs );
8305 }
8306
8307 if ( $delete_network_common_data ) {
8308 $this->_delete_plans( false );
8309
8310 $this->_delete_licenses( false );
8311
8312 // Delete add-ons related to plugin's account.
8313 $this->_delete_account_addons( false );
8314 }
8315
8316 // @todo Delete plans and licenses of add-ons.
8317
8318 self::$_accounts->store();
8319
8320 /**
8321 * IMPORTANT:
8322 * Clear crons must be executed before clearing all storage.
8323 * Otherwise, the cron will not be cleared.
8324 */
8325 if ( $delete_network_common_data ) {
8326 $this->clear_sync_cron();
8327 }
8328
8329 $this->clear_install_sync_cron();
8330
8331 // Clear all storage data.
8332 $this->_storage->clear_all( true, array(
8333 'is_delegated_connection',
8334 'connectivity_test',
8335 'is_on',
8336 ), false );
8337
8338 // Send delete event.
8339 $this->get_api_site_scope()->call( '/', 'delete' );
8340
8341 $this->do_action( 'after_account_delete' );
8342 }
8343
8344 /**
8345 * Delete network level account.
8346 *
8347 * @author Vova Feldman (@svovaf)
8348 * @since 2.0.0
8349 *
8350 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8351 */
8352 function delete_network_account_event( $check_user = true ) {
8353 $this->_logger->entrance( 'slug = ' . $this->_slug );
8354
8355 if ( $check_user && ! $this->is_user_admin() ) {
8356 return;
8357 }
8358
8359 $this->do_action( 'before_network_account_delete' );
8360
8361 // Clear all admin notices.
8362 $this->_admin_notices->clear_all_sticky();
8363
8364 $this->_delete_plans( false, false );
8365
8366 $this->_delete_licenses( false );
8367
8368 // Delete add-ons related to plugin's account.
8369 $this->_delete_account_addons( false );
8370
8371 // @todo Delete plans and licenses of add-ons.
8372
8373 self::$_accounts->store( true );
8374
8375 /**
8376 * IMPORTANT:
8377 * Clear crons must be executed before clearing all storage.
8378 * Otherwise, the cron will not be cleared.
8379 */
8380 $this->clear_sync_cron( true );
8381 $this->clear_install_sync_cron( true );
8382
8383 $sites = self::get_sites();
8384
8385 $install_ids = array();
8386 foreach ( $sites as $site ) {
8387 $blog_id = self::get_site_blog_id( $site );
8388
8389 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8390 continue;
8391 }
8392
8393 $install_id = $this->_delete_site( true, $blog_id );
8394
8395 // Clear all storage data.
8396 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8397
8398 if ( FS_Site::is_valid_id( $install_id ) ) {
8399 $install_ids[] = $install_id;
8400 }
8401
8402 switch_to_blog( $blog_id );
8403
8404 $this->do_action( 'after_account_delete' );
8405
8406 restore_current_blog();
8407 }
8408
8409 $this->_storage->clear_all( true, array(
8410 'connectivity_test',
8411 'is_on',
8412 ), true );
8413
8414 // Send delete event.
8415 if ( ! empty( $install_ids ) ) {
8416 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8417 }
8418
8419 $this->do_action( 'after_network_account_delete' );
8420 }
8421
8422 /**
8423 * Plugin deactivation hook.
8424 *
8425 * @author Vova Feldman (@svovaf)
8426 * @since 1.0.1
8427 */
8428 function _deactivate_plugin_hook() {
8429 $this->_logger->entrance( 'slug = ' . $this->_slug );
8430
8431 if ( ! $this->is_user_admin() ) {
8432 return;
8433 }
8434
8435 $is_network_deactivation = fs_is_network_admin();
8436 $storage_keys_for_removal = array();
8437
8438 $this->_admin_notices->clear_all_sticky();
8439
8440 $storage_keys_for_removal[] = 'sticky_optin_added';
8441 if ( isset( $this->_storage->sticky_optin_added ) ) {
8442 unset( $this->_storage->sticky_optin_added );
8443 }
8444
8445 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8446 // Remember that plugin was already installed.
8447 $this->_storage->is_plugin_new_install = false;
8448 }
8449
8450 // Hook to plugin uninstall.
8451 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8452
8453 $this->clear_module_main_file_cache();
8454 $this->clear_sync_cron( $this->_is_network_active );
8455 $this->clear_install_sync_cron();
8456
8457 if ( $this->is_registered() ) {
8458 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8459 FS_Plugin_Updater::instance( $this )->delete_update_data();
8460 }
8461
8462 if ( $is_network_deactivation ) {
8463 // Send deactivation event.
8464 $this->sync_installs( array(
8465 'is_active' => false,
8466 ) );
8467 } else {
8468 // Send deactivation event.
8469 $this->sync_install( array(
8470 'is_active' => false,
8471 ) );
8472 }
8473 } else {
8474 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8475 // Reset connectivity test cache.
8476 $this->clear_connectivity_info();
8477
8478 $storage_keys_for_removal[] = 'connectivity_test';
8479 }
8480 }
8481
8482 if ( $is_network_deactivation ) {
8483 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8484 unset( $this->_storage->sticky_optin_added_ms );
8485 }
8486
8487 if ( ! empty( $storage_keys_for_removal ) ) {
8488 $sites = self::get_sites();
8489
8490 foreach ( $sites as $site ) {
8491 $blog_id = self::get_site_blog_id( $site );
8492
8493 foreach ( $storage_keys_for_removal as $key ) {
8494 $this->_storage->remove( $key, false, $blog_id );
8495 }
8496
8497 $this->_storage->save( $blog_id );
8498 }
8499 }
8500 }
8501
8502 // Clear API cache on deactivation.
8503 FS_Api::clear_cache();
8504
8505 $this->remove_sdk_reference();
8506 }
8507
8508 /**
8509 * @author Vova Feldman (@svovaf)
8510 * @since 1.1.6
8511 */
8512 private function remove_sdk_reference() {
8513 global $fs_active_plugins;
8514
8515 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8516 if ( $this->_plugin_basename == $data->plugin_path ) {
8517 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8518 break;
8519 }
8520 }
8521
8522 fs_fallback_to_newest_active_sdk();
8523 }
8524
8525 /**
8526 * @author Vova Feldman (@svovaf)
8527 * @since 1.1.3
8528 *
8529 * @param bool $is_anonymous
8530 * @param bool|int $network_or_blog_id Since 2.0.0
8531 */
8532 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8533 // Store information regarding skip to try and opt-in the user
8534 // again in the future.
8535 $skip_info = array(
8536 'is' => $is_anonymous,
8537 'timestamp' => WP_FS__SCRIPT_START_TIME,
8538 'version' => $this->get_plugin_version(),
8539 );
8540
8541 if ( true === $network_or_blog_id ) {
8542 $this->_storage->is_anonymous_ms = $skip_info;
8543 } else {
8544 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8545 }
8546
8547 $this->network_upgrade_mode_completed();
8548
8549 // Update anonymous mode cache.
8550 $this->_is_anonymous = $is_anonymous;
8551 }
8552
8553 /**
8554 * @author Vova Feldman (@svovaf)
8555 * @since 2.5.1
8556 *
8557 * @param bool|int $network_or_blog_id
8558 */
8559 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8560 if ( true === $network_or_blog_id ) {
8561 unset( $this->_storage->is_anonymous_ms );
8562 } else {
8563 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8564 }
8565 }
8566
8567 /**
8568 * @author Vova Feldman (@svovaf)
8569 * @since 2.0.0
8570 *
8571 * @param int $blog_id Site ID.
8572 * @param int $user_id User ID.
8573 * @param string $domain Site domain.
8574 * @param string $path Site path.
8575 * @param int $network_id Network ID. Only relevant on multi-network installations.
8576 * @param array $meta Metadata. Used to set initial site options.
8577 *
8578 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8579 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8580 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8581 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8582 */
8583 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8584 $this->_logger->entrance();
8585
8586 if ( ! $this->_is_network_active ) {
8587 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8588 return;
8589 }
8590
8591 $site = null;
8592 $new_blog_id = $blog_id;
8593
8594 if ( $this->is_premium() &&
8595 $this->is_network_connected() &&
8596 is_object( $this->_license ) &&
8597 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8598 $this->is_license_network_active( $blog_id )
8599 ) {
8600 /**
8601 * 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.
8602 */
8603 $current_blog_id = get_current_blog_id();
8604 $license = clone $this->_license;
8605
8606 $this->switch_to_blog( $blog_id );
8607
8608 // Opt-in with network user.
8609 $this->install_with_user(
8610 $this->get_network_user(),
8611 $license->secret_key,
8612 false,
8613 false,
8614 false
8615 );
8616
8617 if ( is_object( $this->_site ) ) {
8618 if ( $this->_site->license_id == $license->id ) {
8619 /**
8620 * If the license was activated successfully, sync the license data from the remote server.
8621 */
8622 $this->_license = $license;
8623 $this->sync_site_license();
8624 }
8625 }
8626
8627 $site = $this->_site;
8628
8629 $this->switch_to_blog( $current_blog_id );
8630
8631 if ( is_object( $site ) ) {
8632 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8633
8634 // Already connected (with or without a license), so no need to continue.
8635 return;
8636 }
8637 }
8638
8639 if ( $this->is_network_anonymous() ) {
8640 /**
8641 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8642 */
8643 $this->skip_site_connection( $blog_id );
8644 } else if ( $this->is_network_delegated_connection() ) {
8645 /**
8646 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8647 */
8648 $this->delegate_site_connection( $blog_id );
8649 } else if ( $this->is_network_connected() ) {
8650 /**
8651 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8652 */
8653 $current_blog_id = get_current_blog_id();
8654
8655 $this->switch_to_blog( $blog_id );
8656
8657 // Opt-in with network user.
8658 $this->install_with_user(
8659 $this->get_network_user(),
8660 false,
8661 false,
8662 false,
8663 false
8664 );
8665
8666 $site = $this->_site;
8667
8668 $this->switch_to_blog( $current_blog_id );
8669 } else {
8670 /**
8671 * If the super-admin mixed different options (connect, skip, delegated):
8672 * a) If at least one site connection was delegated, then automatically delegate connection.
8673 * 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.
8674 */
8675 $has_delegated_site = false;
8676
8677 $sites = self::get_sites();
8678 foreach ( $sites as $wp_site ) {
8679 $blog_id = self::get_site_blog_id( $wp_site );
8680
8681 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8682 $has_delegated_site = true;
8683 break;
8684 }
8685 }
8686
8687 if ( $has_delegated_site ) {
8688 $this->delegate_site_connection( $blog_id );
8689 } else {
8690 $this->skip_site_connection( $blog_id );
8691 }
8692 }
8693
8694 /**
8695 * 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.
8696 *
8697 * @author Leo Fajardo (@leorw)
8698 * @since 2.5.0
8699 */
8700 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8701 }
8702
8703 /**
8704 * @author Vova Feldman (@svovaf)
8705 * @since 2.5.0
8706 *
8707 * @param \WP_Site $new_site
8708 * @param array $args
8709 */
8710 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8711 $this->_logger->entrance();
8712
8713 $this->_after_new_blog_callback(
8714 $new_site->id,
8715 // Dummy user ID (not in use).
8716 0,
8717 $new_site->domain,
8718 $new_site->path,
8719 $new_site->network_id,
8720 // Dummy meta, not in use.
8721 array()
8722 );
8723 }
8724
8725 /**
8726 * @author Vova Feldman (@svovaf)
8727 * @since 1.1.3
8728 *
8729 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8730 */
8731 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8732 if ( true === $network_or_blog_ids ) {
8733 $this->unset_anonymous_mode( true );
8734
8735 if ( fs_is_network_admin() ) {
8736 $this->_is_anonymous = null;
8737 }
8738
8739 // Rest anonymous mode for all non-delegated sub-sites.
8740 $blog_ids = $this->get_non_delegated_blog_ids();
8741 }
8742 else
8743 {
8744 if ( false === $network_or_blog_ids ) {
8745 $network_or_blog_ids = 0;
8746 }
8747
8748 $blog_ids = is_array( $network_or_blog_ids ) ?
8749 $network_or_blog_ids :
8750 array( $network_or_blog_ids );
8751
8752 foreach ( $blog_ids as $blog_id ) {
8753 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8754 $this->_is_anonymous = null;
8755 }
8756 }
8757 }
8758
8759 foreach ( $blog_ids as $blog_id ) {
8760 $this->unset_anonymous_mode( $blog_id );
8761 }
8762
8763 /**
8764 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8765 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8766 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8767 *
8768 * @author Leo Fajardo (@leorw)
8769 * @since 1.2.2
8770 */
8771 if ( ! $this->_is_network_active ) {
8772 $this->_is_anonymous = null;
8773 }
8774 }
8775
8776 /**
8777 * @author Leo Fajardo (@leorw)
8778 * @since 2.5.3
8779 */
8780 private function update_license_required_permissions_if_anonymous() {
8781 if ( ! $this->is_anonymous() ) {
8782 return;
8783 }
8784
8785 $this->reset_anonymous_mode( fs_is_network_admin() );
8786
8787 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8788 'essentials' => true,
8789 'events' => true,
8790 'diagnostic' => false,
8791 'extensions' => false,
8792 'site' => false,
8793 ) );
8794 }
8795
8796 /**
8797 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8798 * deleting the account in the network level, the URL of the page to redirect to is correct.
8799 *
8800 * @author Leo Fajardo (@leorw)
8801 *
8802 * @since 2.1.3
8803 */
8804 private function maybe_set_slug_and_network_menu_exists_flag() {
8805 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8806 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8807 $this->_menu->get_slug() :
8808 $this->_slug
8809 );
8810 }
8811 }
8812
8813 /**
8814 * Clears the anonymous mode and redirects to the opt-in screen.
8815 *
8816 * @author Vova Feldman (@svovaf)
8817 * @since 1.1.7
8818 */
8819 function connect_again() {
8820 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8821 return;
8822 }
8823
8824 if ( $this->is_anonymous() ) {
8825 $this->reset_anonymous_mode( fs_is_network_admin() );
8826 }
8827
8828 $activation_url_params = array();
8829
8830 if ( $this->is_pending_activation() ) {
8831 $this->clear_pending_activation_mode();
8832
8833 if ( fs_request_get_bool( 'require_license' ) ) {
8834 $activation_url_params['require_license'] = true;
8835 }
8836 }
8837
8838 $this->maybe_set_slug_and_network_menu_exists_flag();
8839
8840 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8841 }
8842
8843 /**
8844 * Skip account connect, and set anonymous mode.
8845 *
8846 * @author Vova Feldman (@svovaf)
8847 * @since 1.1.1
8848 *
8849 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8850 */
8851 function skip_connection( $network_or_blog_ids = false ) {
8852 $this->_logger->entrance();
8853
8854 $this->_admin_notices->remove_sticky( 'connect_account' );
8855
8856 if ( true === $network_or_blog_ids ) {
8857 $this->set_anonymous_mode( true, true );
8858
8859 if ( fs_is_network_admin() ) {
8860 $this->_is_anonymous = null;
8861 }
8862
8863 // Rest anonymous mode for all non-delegated sub-sites.
8864 $blog_ids = $this->get_non_delegated_blog_ids();
8865 }
8866 else
8867 {
8868 if ( false === $network_or_blog_ids ) {
8869 $network_or_blog_ids = 0;
8870 }
8871
8872 $blog_ids = is_array( $network_or_blog_ids ) ?
8873 $network_or_blog_ids :
8874 array( $network_or_blog_ids );
8875
8876 foreach ( $blog_ids as $blog_id ) {
8877 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8878 $this->_is_anonymous = null;
8879 }
8880 }
8881 }
8882
8883 foreach ( $blog_ids as $blog_id ) {
8884 $this->skip_site_connection( $blog_id );
8885 }
8886
8887 $this->network_upgrade_mode_completed();
8888 }
8889
8890 /**
8891 * Skip connection for specific site in the network.
8892 *
8893 * @author Vova Feldman (@svovaf)
8894 * @since 2.0.0
8895 *
8896 * @param int|null $blog_id
8897 * @param bool $send_skip
8898 */
8899 private function skip_site_connection( $blog_id = null ) {
8900 $this->_logger->entrance();
8901
8902 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8903
8904 $this->set_anonymous_mode( true, $blog_id );
8905 }
8906
8907 /**
8908 * Plugin version update hook.
8909 *
8910 * @author Vova Feldman (@svovaf)
8911 * @since 1.0.4
8912 */
8913 private function update_plugin_version_event() {
8914 $this->_logger->entrance();
8915
8916 if ( ! $this->is_registered() ) {
8917 return;
8918 }
8919
8920 $this->schedule_install_sync();
8921 // $this->sync_install( array(), true );
8922 }
8923
8924 /**
8925 * Generate an MD5 signature of a plugins collection.
8926 * This helper methods used to identify changes in a plugins collection.
8927 *
8928 * @author Vova Feldman (@svovaf)
8929 * @since 2.0.0
8930 *
8931 * @param array [string]array $plugins
8932 *
8933 * @return string
8934 */
8935 private function get_plugins_thumbprint( $plugins ) {
8936 ksort( $plugins );
8937
8938 $thumbprint = '';
8939 foreach ( $plugins as $basename => $data ) {
8940 $thumbprint .= $data['slug'] . ',' .
8941 $data['Version'] . ',' .
8942 ( $data['is_active'] ? '1' : '0' ) . ';';
8943 }
8944
8945 return md5( $thumbprint );
8946 }
8947
8948 /**
8949 * Return a list of modified plugins since the last sync.
8950 *
8951 * Note:
8952 * There's no point to store a plugins counter since even if the number of
8953 * plugins didn't change, we still need to check if the versions are all the
8954 * same and the activity state is similar.
8955 *
8956 * @author Vova Feldman (@svovaf)
8957 * @since 1.1.8
8958 *
8959 * @return array|false
8960 */
8961 private function get_plugins_data_for_api() {
8962 // Alias.
8963 $site_active_plugins_option_name = 'active_plugins';
8964 $network_plugins_option_name = 'all_plugins';
8965
8966 /**
8967 * Collection of all site level active plugins.
8968 */
8969 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8970
8971 if ( ! is_object( $site_active_plugins_cache ) ) {
8972 $site_active_plugins_cache = (object) array(
8973 'timestamp' => '',
8974 'md5' => '',
8975 'plugins' => array(),
8976 );
8977 }
8978
8979 $time = time();
8980
8981 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8982 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8983 ) {
8984 // Don't send plugin updates if last update was in the past 5 min.
8985 return false;
8986 }
8987
8988 // Write timestamp to lock the logic.
8989 $site_active_plugins_cache->timestamp = $time;
8990 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8991
8992 // Reload options from DB.
8993 self::$_accounts->load( true );
8994 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8995
8996 if ( $time != $site_active_plugins_cache->timestamp ) {
8997 // If timestamp is different, then another thread captured the lock.
8998 return false;
8999 }
9000
9001 /**
9002 * Collection of all plugins (network level).
9003 */
9004 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
9005
9006 if ( ! is_object( $network_plugins_cache ) ) {
9007 $network_plugins_cache = (object) array(
9008 'timestamp' => '',
9009 'md5' => '',
9010 'plugins' => array(),
9011 );
9012 }
9013
9014 // Check if there's a change in plugins.
9015 $network_plugins = self::get_network_plugins();
9016 $site_active_plugins = self::get_site_active_plugins();
9017
9018 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
9019 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
9020
9021 // Check if plugins status changed (version or active/inactive).
9022 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
9023 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
9024
9025 if ( ! $network_plugins_changed &&
9026 ! $site_active_plugins_changed
9027 ) {
9028 // No changes.
9029 return array();
9030 }
9031
9032 $plugins_update_data = array();
9033
9034 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
9035 if ( ! isset( $network_plugins[ $basename ] ) ) {
9036 // Plugin uninstalled.
9037 $uninstalled_plugin_data = $data;
9038 $uninstalled_plugin_data['is_active'] = false;
9039 $uninstalled_plugin_data['is_uninstalled'] = true;
9040 $plugins_update_data[] = $uninstalled_plugin_data;
9041
9042 unset( $network_plugins[ $basename ] );
9043
9044 unset( $network_plugins_cache->plugins[ $basename ] );
9045 unset( $site_active_plugins_cache->plugins[ $basename ] );
9046
9047 continue;
9048 }
9049
9050 $was_active = $data['is_active'] ||
9051 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9052 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
9053 $is_active = $network_plugins[ $basename ]['is_active'] ||
9054 ( isset( $site_active_plugins[ $basename ] ) &&
9055 $site_active_plugins[ $basename ]['is_active'] );
9056
9057 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9058 isset( $site_active_plugins[ $basename ] )
9059 ) {
9060 // Plugin was site level activated.
9061 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
9062 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
9063 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9064 ! isset( $site_active_plugins[ $basename ] )
9065 ) {
9066 // Plugin was site level deactivated.
9067 unset( $site_active_plugins_cache->plugins[ $basename ] );
9068 }
9069
9070 $prev_version = $data['version'];
9071 $current_version = $network_plugins[ $basename ]['Version'];
9072
9073 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9074 // Plugin activated or deactivated, or version changed.
9075
9076 if ( $was_active !== $is_active ) {
9077 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9078 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9079 }
9080 }
9081
9082 if ( $prev_version !== $current_version ) {
9083 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9084 }
9085
9086 $updated_plugin_data = $data;
9087 $updated_plugin_data['is_active'] = $is_active;
9088 $updated_plugin_data['version'] = $current_version;
9089 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9090 $plugins_update_data[] = $updated_plugin_data;
9091 }
9092 }
9093
9094 // Find new plugins that weren't yet seen before.
9095 foreach ( $network_plugins as $basename => $data ) {
9096 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9097 // New plugin.
9098 $new_plugin = array(
9099 'slug' => $data['slug'],
9100 'version' => $data['Version'],
9101 'title' => $data['Name'],
9102 'is_active' => $data['is_active'],
9103 'is_uninstalled' => false,
9104 );
9105
9106 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9107
9108 $is_site_level_active = (
9109 isset( $site_active_plugins[ $basename ] ) &&
9110 $site_active_plugins[ $basename ]['is_active']
9111 );
9112
9113 /**
9114 * If not network active, set the activity status based on the site-level plugin status.
9115 */
9116 if ( ! $new_plugin['is_active'] ) {
9117 $new_plugin['is_active'] = $is_site_level_active;
9118 }
9119
9120 $plugins_update_data[] = $new_plugin;
9121
9122 if ( isset( $site_active_plugins[ $basename ] ) ) {
9123 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9124 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9125 }
9126 }
9127 }
9128
9129 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9130 $site_active_plugins_cache->timestamp = $time;
9131 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9132
9133 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9134 $network_plugins_cache->timestamp = $time;
9135 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9136
9137 return $plugins_update_data;
9138 }
9139
9140 /**
9141 * Return a list of modified themes since the last sync.
9142 *
9143 * Note:
9144 * There's no point to store a themes counter since even if the number of
9145 * themes didn't change, we still need to check if the versions are all the
9146 * same and the activity state is similar.
9147 *
9148 * @author Vova Feldman (@svovaf)
9149 * @since 1.1.8
9150 *
9151 * @return array|false
9152 */
9153 private function get_themes_data_for_api() {
9154 // Alias.
9155 $option_name = 'all_themes';
9156
9157 $all_cached_themes = self::$_accounts->get_option( $option_name );
9158
9159 if ( ! is_object( $all_cached_themes ) ) {
9160 $all_cached_themes = (object) array(
9161 'timestamp' => '',
9162 'md5' => '',
9163 'themes' => array(),
9164 );
9165 }
9166
9167 $time = time();
9168
9169 if ( ! empty( $all_cached_themes->timestamp ) &&
9170 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9171 ) {
9172 // Don't send theme updates if last update was in the past 5 min.
9173 return false;
9174 }
9175
9176 // Write timestamp to lock the logic.
9177 $all_cached_themes->timestamp = $time;
9178 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9179
9180 // Reload options from DB.
9181 self::$_accounts->load( true );
9182 $all_cached_themes = self::$_accounts->get_option( $option_name );
9183
9184 if ( $time != $all_cached_themes->timestamp ) {
9185 // If timestamp is different, then another thread captured the lock.
9186 return false;
9187 }
9188
9189 // Get active theme.
9190 $active_theme = wp_get_theme();
9191 $active_theme_stylesheet = $active_theme->get_stylesheet();
9192
9193 // Check if there's a change in themes.
9194 $all_themes = wp_get_themes();
9195
9196 // Check if themes changed.
9197 ksort( $all_themes );
9198
9199 $themes_signature = '';
9200 foreach ( $all_themes as $slug => $data ) {
9201 $is_active = ( $slug === $active_theme_stylesheet );
9202 $themes_signature .= $slug . ',' .
9203 $data->version . ',' .
9204 ( $is_active ? '1' : '0' ) . ';';
9205 }
9206
9207 // Check if themes status changed (version or active/inactive).
9208 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9209
9210 $themes_update_data = array();
9211
9212 if ( $themes_changed ) {
9213 // Change in themes, report changes.
9214
9215 // Update existing themes info.
9216 foreach ( $all_cached_themes->themes as $slug => $data ) {
9217 $is_active = ( $slug === $active_theme_stylesheet );
9218
9219 if ( ! isset( $all_themes[ $slug ] ) ) {
9220 // Plugin uninstalled.
9221 $uninstalled_theme_data = $data;
9222 $uninstalled_theme_data['is_active'] = false;
9223 $uninstalled_theme_data['is_uninstalled'] = true;
9224 $themes_update_data[] = $uninstalled_theme_data;
9225
9226 unset( $all_themes[ $slug ] );
9227 unset( $all_cached_themes->themes[ $slug ] );
9228 } else if ( $data['is_active'] !== $is_active ||
9229 $data['version'] !== $all_themes[ $slug ]->version
9230 ) {
9231 // Plugin activated or deactivated, or version changed.
9232
9233 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9234 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9235
9236 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9237 }
9238 }
9239
9240 // Find new themes that weren't yet seen before.
9241 foreach ( $all_themes as $slug => $data ) {
9242 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9243 $is_active = ( $slug === $active_theme_stylesheet );
9244
9245 // New plugin.
9246 $new_plugin = array(
9247 'slug' => $slug,
9248 'version' => $data->version,
9249 'title' => $data->name,
9250 'is_active' => $is_active,
9251 'is_uninstalled' => false,
9252 );
9253
9254 $themes_update_data[] = $new_plugin;
9255 $all_cached_themes->themes[ $slug ] = $new_plugin;
9256 }
9257 }
9258
9259 $all_cached_themes->md5 = md5( $themes_signature );
9260 $all_cached_themes->timestamp = time();
9261 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9262 }
9263
9264 return $themes_update_data;
9265 }
9266
9267 /**
9268 * Get site data for API install request.
9269 *
9270 * @author Vova Feldman (@svovaf)
9271 * @since 1.1.2
9272 *
9273 * @param string[] $override
9274 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9275 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9276 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9277 *
9278 * @return array
9279 */
9280 private function get_install_data_for_api(
9281 array $override,
9282 $include_plugins = true,
9283 $include_themes = true,
9284 $include_blog_data = true
9285 ) {
9286 // Alias.
9287 $permissions = FS_Permission_Manager::instance( $this );
9288
9289 if ( $permissions->is_extensions_tracking_allowed() ) {
9290 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9291 /**
9292 * @since 1.1.8 Also send plugin updates.
9293 */
9294 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9295 $plugins = $this->get_plugins_data_for_api();
9296 if ( ! empty( $plugins ) ) {
9297 $override['plugins'] = $plugins;
9298 }
9299 }
9300 }
9301
9302 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9303 /**
9304 * @since 1.1.8 Also send themes updates.
9305 */
9306 if ( $include_themes && ! isset( $override['themes'] ) ) {
9307 $themes = $this->get_themes_data_for_api();
9308 if ( ! empty( $themes ) ) {
9309 $override['themes'] = $themes;
9310 }
9311 }
9312 }
9313 }
9314
9315 $versions = $this->get_versions();
9316
9317 $blog_data = array();
9318 if ( $include_blog_data ) {
9319 $blog_data['url'] = self::get_unfiltered_site_url();
9320
9321 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9322 $blog_data = array_merge( $blog_data, array(
9323 'language' => self::get_sanitized_language(),
9324 'title' => get_bloginfo( 'name' ),
9325 ) );
9326 }
9327 }
9328
9329 return array_merge( $versions, $blog_data, array(
9330 'version' => $this->get_plugin_version(),
9331 'is_premium' => $this->is_premium(),
9332 // Special params.
9333 'is_active' => true,
9334 'is_uninstalled' => false,
9335 ), $override );
9336 }
9337
9338 /**
9339 * Update installs details.
9340 *
9341 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9342 *
9343 * @author Vova Feldman (@svovaf)
9344 * @since 2.0.0
9345 *
9346 * @param string[] string $override
9347 * @param bool $only_diff
9348 * @param bool $is_keepalive
9349 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9350 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9351 *
9352 * @return array
9353 */
9354 private function get_installs_data_for_api(
9355 array $override,
9356 $only_diff = false,
9357 $is_keepalive = false,
9358 $include_plugins = true,
9359 $include_themes = true
9360 ) {
9361 /**
9362 * @since 1.1.8 Also send plugin updates.
9363 */
9364 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9365 // $plugins = $this->get_plugins_data_for_api();
9366 // if ( ! empty( $plugins ) ) {
9367 // $override['plugins'] = $plugins;
9368 // }
9369 // }
9370 /**
9371 * @since 1.1.8 Also send themes updates.
9372 */
9373 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9374 // $themes = $this->get_themes_data_for_api();
9375 // if ( ! empty( $themes ) ) {
9376 // $override['themes'] = $themes;
9377 // }
9378 // }
9379
9380 // Common properties.
9381 $versions = $this->get_versions();
9382 $common = array_merge( $versions, array(
9383 'version' => $this->get_plugin_version(),
9384 'is_premium' => $this->is_premium(),
9385 ), $override );
9386
9387
9388 $is_common_diff_for_any_site = false;
9389 $common_diff_union = array();
9390
9391 $installs_data = array();
9392
9393 $sites = self::get_sites();
9394
9395 $subsite_data_for_api_by_install_id = array();
9396 $install_url_by_install_id = array();
9397 $subsite_registration_date_by_install_id = array();
9398
9399 foreach ( $sites as $site ) {
9400 $blog_id = self::get_site_blog_id( $site );
9401
9402 $install = $this->get_install_by_blog_id( $blog_id );
9403
9404 if ( is_object( $install ) ) {
9405 if ( $install->user_id != $this->_user->id ) {
9406 // Install belongs to a different owner.
9407 continue;
9408 }
9409
9410 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9411 // Don't send updates regarding opted-out installs.
9412 continue;
9413 }
9414
9415 $install_data = $this->get_site_info( $site, true );
9416
9417 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9418 continue;
9419 }
9420
9421 $uid = $install_data['uid'];
9422 $url = $install_data['url'];
9423 $registration_date = $install_data['registration_date'];
9424
9425 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9426 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9427 $clone_install_url = $install_url_by_install_id[ $install->id ];
9428 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9429
9430 $skip = false;
9431
9432 if (
9433 ! empty( $install_data['registration_date'] ) &&
9434 ! empty( $clone_subsite_registration_date )
9435 ) {
9436 /**
9437 * 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.
9438 *
9439 * @author Leo Fajardo (@leorw)
9440 * @since 2.5.1
9441 */
9442 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9443 } else if (
9444 /**
9445 * 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.
9446 *
9447 * @author Leo Fajardo (@leorw)
9448 * @since 2.5.0
9449 */
9450 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9451 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9452 ) {
9453 $skip = true;
9454 }
9455
9456 if ( $skip ) {
9457 // 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.
9458 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9459 continue;
9460 }
9461 }
9462
9463 unset( $install_data['blog_id'] );
9464 unset( $install_data['uid'] );
9465 unset( $install_data['url'] );
9466 unset( $install_data['registration_date'] );
9467
9468 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9469 $install_data['is_uninstalled'] = $install->is_uninstalled;
9470
9471 $common_diff = null;
9472 $is_common_diff = false;
9473 if ( $only_diff ) {
9474 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9475 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9476
9477 $is_common_diff = ! empty( $common_diff );
9478
9479 if ( $is_common_diff ) {
9480 foreach ( $common_diff as $k => $v ) {
9481 if ( ! isset( $common_diff_union[ $k ] ) ) {
9482 $common_diff_union[ $k ] = $v;
9483 }
9484 }
9485 }
9486
9487 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9488 }
9489
9490 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9491 // Add install ID and site unique ID.
9492 $install_data['id'] = $install->id;
9493 $install_data['uid'] = $uid;
9494 $install_data['url'] = $url;
9495
9496 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9497 $install_url_by_install_id[ $install->id ] = $install->url;
9498 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9499 }
9500 }
9501 }
9502
9503 restore_current_blog();
9504
9505 $installs_data = array_merge(
9506 $installs_data,
9507 array_values( $subsite_data_for_api_by_install_id )
9508 );
9509
9510 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9511 if ( ! $only_diff ) {
9512 $installs_data[] = $common;
9513 } else if ( ! empty( $common_diff_union ) ) {
9514 $installs_data[] = $common_diff_union;
9515 }
9516 }
9517
9518 foreach ( $installs_data as &$data ) {
9519 $data = (object) $data;
9520 }
9521
9522 return $installs_data;
9523 }
9524
9525 /**
9526 * Compare site actual data to the stored install data and return the differences for an API data sync.
9527 *
9528 * @author Vova Feldman (@svovaf)
9529 * @since 2.0.0
9530 *
9531 * @param array $site
9532 * @param FS_Site $install
9533 * @param string[] string $override
9534 *
9535 * @return array
9536 */
9537 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9538 $diff = array();
9539 $special = array();
9540 $special_override = false;
9541
9542 foreach ( $site as $p => $v ) {
9543 if ( property_exists( $install, $p ) ) {
9544 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9545 $install->{$p} != $v
9546 ) {
9547 $val = self::get_api_sanitized_property( $p, $v );
9548
9549 if ( $install->{$p} != $val ) {
9550 $install->{$p} = $val;
9551 $diff[ $p ] = $val;
9552 }
9553 }
9554 } else {
9555 $special[ $p ] = $v;
9556
9557 if ( isset( $override[ $p ] ) ||
9558 'plugins' === $p ||
9559 'themes' === $p
9560 ) {
9561 $special_override = true;
9562 }
9563 }
9564 }
9565
9566 if ( $special_override || 0 < count( $diff ) ) {
9567 // Add special params only if has at least one
9568 // standard param, or if explicitly requested to
9569 // override a special param or a param which is not exist
9570 // in the install object.
9571 $diff = array_merge( $diff, $special );
9572 }
9573
9574 return $diff;
9575 }
9576
9577 /**
9578 * @author Leo Fajardo (@leorw)
9579 * @since 2.5.1
9580 */
9581 private function send_pending_clone_update_once() {
9582 $this->_logger->entrance();
9583
9584 if ( ! empty( $this->_storage->clone_id ) ) {
9585 return;
9586 }
9587
9588 $install_clone = $this->get_api_site_scope()->call(
9589 '/clones',
9590 'post',
9591 array( 'site_url' => self::get_unfiltered_site_url() )
9592 );
9593
9594 if ( $this->is_api_result_entity( $install_clone ) ) {
9595 $this->_storage->clone_id = $install_clone->id;
9596 }
9597 }
9598
9599 /**
9600 * @author Leo Fajardo (@leorw)
9601 * @since 2.5.1
9602 *
9603 * @param string $resolution_type
9604 * @param FS_Site $clone_context_install
9605 */
9606 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9607 $this->_logger->entrance();
9608
9609 if ( empty( $this->_storage->clone_id ) ) {
9610 return;
9611 }
9612
9613 $new_install_id = null;
9614 $current_site = null;
9615
9616 $flush = false;
9617
9618 /**
9619 * 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.
9620 */
9621 if ( $clone_context_install->id != $this->_site->id ) {
9622 $new_install_id = $this->_site->id;
9623 $current_site = $this->_site;
9624 $this->_site = $clone_context_install;
9625
9626 $flush = true;
9627 }
9628
9629 $this->get_api_site_scope( $flush )->call(
9630 "/clones/{$this->_storage->clone_id}",
9631 'put',
9632 array(
9633 'resolution' => $resolution_type,
9634 'new_install_id' => $new_install_id,
9635 )
9636 );
9637
9638 if ( is_object( $current_site ) ) {
9639 /**
9640 * Ensure that the install scope entity is updated back to the previous install entity.
9641 */
9642 $this->_site = $current_site;
9643
9644 // Restore the previous install scope entity of the API.
9645 $this->get_api_site_scope( true );
9646 }
9647 }
9648
9649 /**
9650 * Update install only if changed.
9651 *
9652 * @author Vova Feldman (@svovaf)
9653 * @since 1.0.9
9654 *
9655 * @param string[] string $override
9656 * @param bool $flush
9657 * @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.
9658 *
9659 * @return false|object|string
9660 */
9661 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9662 $this->_logger->entrance();
9663
9664 $check_properties = $this->get_install_data_for_api( $override );
9665
9666 if ( $flush ) {
9667 $params = $check_properties;
9668 } else {
9669 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9670 }
9671
9672 if ( empty( $params ) ) {
9673 $keepalive_only_update = $this->should_send_keepalive_update();
9674
9675 if ( ! $keepalive_only_update ) {
9676 /**
9677 * There are no updates to send including keepalive.
9678 *
9679 * @author Leo Fajardo (@leorw)
9680 * @since 2.2.3
9681 */
9682 return false;
9683 }
9684 }
9685
9686 if ( $is_two_way_sync ) {
9687 /**
9688 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9689 *
9690 * @author Leo Fajardo (@leorw)
9691 * @since 2.2.3
9692 */
9693 if ( ! is_multisite() ) {
9694 // Update last install sync timestamp.
9695 $this->set_cron_execution_timestamp( 'install_sync' );
9696 }
9697
9698 $params['uid'] = $this->get_anonymous_id();
9699 }
9700
9701 $this->set_keepalive_timestamp();
9702
9703 // Send updated values to FS.
9704 $site = $this->api_site_call( '/', 'put', $params, true );
9705
9706 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9707 /**
9708 * Clear scheduled install sync after a two-way sync call.
9709 *
9710 * @author Leo Fajardo (@leorw)
9711 * @since 2.2.3
9712 */
9713 if ( ! is_multisite() ) {
9714 // I successfully sent install update, clear scheduled sync if exist.
9715 $this->clear_install_sync_cron();
9716 }
9717 }
9718
9719 return $site;
9720 }
9721
9722 /**
9723 * Update installs only if changed.
9724 *
9725 * @author Vova Feldman (@svovaf)
9726 * @since 2.0.0
9727 *
9728 * @param string[] string $override
9729 * @param bool $flush
9730 * @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.
9731 *
9732 * @return false|object|string
9733 */
9734 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9735 $this->_logger->entrance();
9736
9737 /**
9738 * Pass `true` to use the network level storage since the update is for many installs.
9739 *
9740 * @author Leo Fajardo (@leorw)
9741 * @since 2.2.3
9742 */
9743 $should_send_keepalive = $this->should_send_keepalive_update( true );
9744
9745 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9746
9747 if ( empty( $installs_data ) ) {
9748 return false;
9749 }
9750
9751 if ( $is_two_way_sync ) {
9752 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9753 $this->set_cron_execution_timestamp( 'install_sync' );
9754 }
9755
9756 /**
9757 * Pass `true` to use the network level storage since the update is for many installs.
9758 *
9759 * @author Leo Fajardo (@leorw)
9760 * @since 2.2.3
9761 */
9762 $this->set_keepalive_timestamp( true );
9763
9764 // Send updated values to FS.
9765 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9766
9767 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9768 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9769 $this->clear_install_sync_cron();
9770 }
9771
9772 return $result;
9773 }
9774
9775 /**
9776 * @author Leo Fajardo (@leorw)
9777 *
9778 * @param bool|null $use_network_level_storage
9779 *
9780 * @return bool
9781 */
9782 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9783 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9784
9785 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9786 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9787 return true;
9788 } else {
9789 // 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.
9790 return ( 7 == rand( 1, 7 ) );
9791 }
9792 }
9793
9794 /**
9795 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9796 *
9797 * @author Leo Fajardo (@leorw)
9798 * @since 2.3.2
9799 */
9800 private function maybe_sync_install_user() {
9801 if ( $this->_user->id == $this->_site->user_id ) {
9802 return;
9803 }
9804
9805 // Fetch user data and store if found.
9806 $this->sync_user_by_current_install();
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 function sync_install( $override = array(), $flush = false ) {
9819 $this->_logger->entrance();
9820
9821 $site = $this->send_install_update( $override, $flush, true );
9822
9823 if ( false === $site ) {
9824 // No sync required.
9825 return;
9826 }
9827
9828 if ( ! $this->is_api_result_entity( $site ) ) {
9829 // Failed to sync, don't update locally.
9830 return;
9831 }
9832
9833 $this->_site = new FS_Site( $site );
9834
9835 $this->_store_site( true );
9836 }
9837
9838 /**
9839 * Update install only if changed.
9840 *
9841 * @author Vova Feldman (@svovaf)
9842 * @since 1.0.9
9843 *
9844 * @param string[] string $override
9845 * @param bool $flush
9846 */
9847 private function sync_installs( $override = array(), $flush = false ) {
9848 $this->_logger->entrance();
9849
9850 $result = $this->send_installs_update( $override, $flush, true );
9851
9852 if ( false === $result ) {
9853 // No sync required.
9854 return;
9855 }
9856
9857 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9858 // Failed to sync, don't update locally.
9859 return;
9860 }
9861
9862 $address_to_blog_map = $this->get_address_to_blog_map();
9863
9864 foreach ( $result->installs as $install ) {
9865 $this->_site = new FS_Site( $install );
9866
9867 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9868 $blog_id = $address_to_blog_map[ $address ];
9869
9870 $this->_store_site( true, $blog_id );
9871 }
9872 }
9873
9874 /**
9875 * Track install's custom event.
9876 *
9877 * IMPORTANT:
9878 * Custom event tracking is currently only supported for specific clients.
9879 * If you are not one of them, please don't use this method. If you will,
9880 * the API will simply ignore your request based on the plugin ID.
9881 *
9882 * Need custom tracking for your plugin or theme?
9883 * If you are interested in custom event tracking please contact yo@freemius.com
9884 * for further details.
9885 *
9886 * @author Vova Feldman (@svovaf)
9887 * @since 1.2.1
9888 *
9889 * @param string $name Event name.
9890 * @param array $properties Associative key/value array with primitive values only
9891 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9892 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9893 *
9894 * @return object|false Event data or FALSE on failure.
9895 *
9896 * @throws \Freemius_InvalidArgumentException
9897 */
9898 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9899 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9900
9901 if ( ! $this->is_registered() ) {
9902 return false;
9903 }
9904
9905 $event = array( 'type' => $name );
9906
9907 if ( is_numeric( $process_at ) && $process_at > time() ) {
9908 $event['process_at'] = $process_at;
9909 }
9910
9911 if ( $once ) {
9912 $event['once'] = true;
9913 }
9914
9915 if ( ! empty( $properties ) ) {
9916 // Verify associative array values are primitive.
9917 foreach ( $properties as $k => $v ) {
9918 if ( ! is_scalar( $v ) ) {
9919 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9920 }
9921 }
9922
9923 $event['properties'] = $properties;
9924 }
9925
9926 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9927
9928 return $this->is_api_error( $result ) ?
9929 false :
9930 $result;
9931 }
9932
9933 /**
9934 * Track install's custom event only once, but it still triggers the API call.
9935 *
9936 * IMPORTANT:
9937 * Custom event tracking is currently only supported for specific clients.
9938 * If you are not one of them, please don't use this method. If you will,
9939 * the API will simply ignore your request based on the plugin ID.
9940 *
9941 * Need custom tracking for your plugin or theme?
9942 * If you are interested in custom event tracking please contact yo@freemius.com
9943 * for further details.
9944 *
9945 * @author Vova Feldman (@svovaf)
9946 * @since 1.2.1
9947 *
9948 * @param string $name Event name.
9949 * @param array $properties Associative key/value array with primitive values only
9950 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9951 *
9952 * @return object|false Event data or FALSE on failure.
9953 *
9954 * @throws \Freemius_InvalidArgumentException
9955 *
9956 * @user Freemius::track_event()
9957 */
9958 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9959 return $this->track_event( $name, $properties, $process_at, true );
9960 }
9961
9962 /**
9963 * Plugin uninstall hook.
9964 *
9965 * @author Vova Feldman (@svovaf)
9966 * @since 1.0.1
9967 *
9968 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9969 */
9970 function _uninstall_plugin_event( $check_user = true ) {
9971 $this->_logger->entrance( 'slug = ' . $this->_slug );
9972
9973 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9974 return;
9975 }
9976
9977 $params = array();
9978 $uninstall_reason = null;
9979 if ( isset( $this->_storage->uninstall_reason ) ) {
9980 $uninstall_reason = $this->_storage->uninstall_reason;
9981 $params['reason_id'] = $uninstall_reason->id;
9982 $params['reason_info'] = $uninstall_reason->info;
9983 }
9984
9985 if ( ! $this->is_registered() ) {
9986 // Send anonymous uninstall event only if user submitted a feedback.
9987 if ( isset( $uninstall_reason ) ) {
9988 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9989 $this->opt_in( false, false, false, false, true );
9990 } else {
9991 $params['uid'] = $this->get_anonymous_id();
9992 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9993 }
9994 }
9995 } else {
9996 $params = array_merge( $params, array(
9997 'is_active' => false,
9998 'is_uninstalled' => true,
9999 ) );
10000
10001 if ( $this->_is_network_active ) {
10002 // Send uninstall event.
10003 $this->send_installs_update( $params );
10004 } else {
10005 // Send uninstall event and handle the result.
10006 $this->sync_install( $params );
10007 }
10008 }
10009
10010 // @todo Decide if we want to delete plugin information from db.
10011 }
10012
10013 /**
10014 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
10015 *
10016 * @author Vova Feldman (@svovaf)
10017 * @since 2.2.1
10018 *
10019 * @param string $is_premium
10020 * @param string $caller
10021 *
10022 * @return void
10023 */
10024 function set_basename( $is_premium, $caller ) {
10025 $basename = plugin_basename( $caller );
10026
10027 $current_basename = $is_premium ?
10028 $this->_premium_plugin_basename :
10029 $this->_free_plugin_basename;
10030
10031 if ( $current_basename == $basename ) {
10032 // Basename value set correctly.
10033 return;
10034 }
10035
10036 if ( $is_premium ) {
10037 $this->_premium_plugin_basename = $basename;
10038 } else {
10039 $this->_free_plugin_basename = $basename;
10040 }
10041
10042 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
10043
10044 register_activation_hook(
10045 $plugin_dir . $basename,
10046 array( &$this, '_activate_plugin_event_hook' )
10047 );
10048 }
10049
10050 /**
10051 * @author Vova Feldman (@svovaf)
10052 * @since 1.1.1
10053 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
10054 *
10055 * @return string
10056 */
10057 function premium_plugin_basename() {
10058 if ( ! isset( $this->_premium_plugin_basename ) ) {
10059 $this->_premium_plugin_basename = $this->is_premium() ?
10060 // The product is premium, so use the current basename.
10061 $this->_plugin_basename :
10062 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
10063 }
10064
10065 return $this->_premium_plugin_basename;
10066 }
10067
10068 /**
10069 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
10070 *
10071 * @author Vova Feldman (@svovaf)
10072 * @since 1.0.2
10073 */
10074 public static function _uninstall_plugin_hook() {
10075 self::_load_required_static();
10076
10077 self::$_static_logger->entrance();
10078
10079 if ( ! current_user_can( 'activate_plugins' ) ) {
10080 return;
10081 }
10082
10083 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
10084
10085 self::$_static_logger->info( 'plugin = ' . $plugin_file );
10086
10087 define( 'WP_FS__UNINSTALL_MODE', true );
10088
10089 $fs = self::get_instance_by_file( $plugin_file );
10090
10091 if ( is_object( $fs ) ) {
10092 $fs->remove_sdk_reference();
10093
10094 self::require_plugin_essentials();
10095
10096 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
10097 is_plugin_active( $fs->premium_plugin_basename() )
10098 ) {
10099 // Deleting Free or Premium plugin version while the other version still installed.
10100 return;
10101 }
10102
10103 if (
10104 ! $fs->is_clone() &&
10105 /**
10106 * 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).
10107 *
10108 * @author Leo Fajardo
10109 */
10110 ( ! is_object( $fs->_site ) || $fs->is_registered() )
10111 ) {
10112 $fs->_uninstall_plugin_event();
10113 }
10114
10115 $fs->do_action( 'after_uninstall' );
10116 }
10117 }
10118
10119 #----------------------------------------------------------------------------------
10120 #region Plugin Information
10121 #----------------------------------------------------------------------------------
10122
10123 /**
10124 * Load WordPress core plugin.php essential module.
10125 *
10126 * @author Vova Feldman (@svovaf)
10127 * @since 1.1.1
10128 */
10129 private static function require_plugin_essentials() {
10130 if ( ! function_exists( 'get_plugins' ) ) {
10131 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
10132
10133 require_once ABSPATH . 'wp-admin/includes/plugin.php';
10134 }
10135 }
10136
10137 /**
10138 * Load WordPress core pluggable.php module.
10139 *
10140 * @author Vova Feldman (@svovaf)
10141 * @since 1.1.2
10142 */
10143 private static function require_pluggable_essentials() {
10144 if ( ! function_exists( 'wp_get_current_user' ) ) {
10145 require_once ABSPATH . 'wp-includes/pluggable.php';
10146 }
10147 }
10148
10149 /**
10150 * Return plugin data.
10151 *
10152 * @author Vova Feldman (@svovaf)
10153 * @since 1.0.1
10154 *
10155 * @param bool $reparse_plugin_metadata
10156 *
10157 * @return array
10158 */
10159 function get_plugin_data( $reparse_plugin_metadata = false ) {
10160 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
10161 self::require_plugin_essentials();
10162
10163 if ( $this->is_plugin() ) {
10164 /**
10165 * @author Vova Feldman (@svovaf)
10166 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
10167 *
10168 * @link https://github.com/Freemius/wordpress-sdk/issues/77
10169 */
10170 $plugin_data = get_plugin_data(
10171 $this->_plugin_main_file_path,
10172 false,
10173 false
10174 );
10175 } else {
10176 $theme_data = wp_get_theme();
10177
10178 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
10179 $parent_theme = $theme_data->parent();
10180
10181 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
10182 $theme_data = $parent_theme;
10183 }
10184 }
10185
10186 $plugin_data = array(
10187 'Name' => $theme_data->get( 'Name' ),
10188 'Version' => $theme_data->get( 'Version' ),
10189 'Author' => $theme_data->get( 'Author' ),
10190 'Description' => $theme_data->get( 'Description' ),
10191 'PluginURI' => $theme_data->get( 'ThemeURI' ),
10192 );
10193 }
10194
10195 $this->_plugin_data = $plugin_data;
10196 }
10197
10198 return $this->_plugin_data;
10199 }
10200
10201 /**
10202 * @author Vova Feldman (@svovaf)
10203 * @since 1.0.1
10204 * @since 1.2.2.5 If slug not set load slug by module ID.
10205 *
10206 * @return string Plugin slug.
10207 */
10208 function get_slug() {
10209 if ( ! isset( $this->_slug ) ) {
10210 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10211 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10212 }
10213
10214 return $this->_slug;
10215 }
10216
10217 /**
10218 * @author Leo Fajardo (@leorw)
10219 * @since 2.2.1
10220 *
10221 * @return string
10222 */
10223 function get_premium_slug() {
10224 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10225 $this->_plugin->premium_slug :
10226 "{$this->_slug}-premium";
10227 }
10228
10229 /**
10230 * Retrieve the desired folder name for the product.
10231 *
10232 * @author Vova Feldman (@svovaf)
10233 * @since 1.2.1.7
10234 *
10235 * @return string Plugin slug.
10236 */
10237 function get_target_folder_name() {
10238 return $this->can_use_premium_code() ?
10239 $this->_plugin->premium_slug :
10240 $this->_slug;
10241 }
10242
10243 /**
10244 * @author Vova Feldman (@svovaf)
10245 * @since 1.0.1
10246 *
10247 * @return number Plugin ID.
10248 */
10249 function get_id() {
10250 return $this->_plugin->id;
10251 }
10252
10253 /**
10254 * @author Leo Fajardo (@leorw)
10255 * @since 2.2.4
10256 *
10257 * @return number|null Bundle ID.
10258 */
10259 function get_bundle_id() {
10260 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10261 $this->_plugin->bundle_id :
10262 null;
10263 }
10264
10265 /**
10266 * @author Vova Feldman (@svovaf)
10267 * @since 2.3.1
10268 *
10269 * @return string|null Bundle public key.
10270 */
10271 function get_bundle_public_key() {
10272 return isset( $this->_plugin->bundle_public_key ) ?
10273 $this->_plugin->bundle_public_key :
10274 null;
10275 }
10276
10277 /**
10278 * Get whether the SDK has been initiated in the context of a Bundle.
10279 *
10280 * This will return true, if `bundle_id` is present in the SDK init parameters.
10281 *
10282 * ```php
10283 * $my_fs = fs_dynamic_init( array(
10284 * // ...
10285 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10286 * 'bundle_public_key' => 'pk_XXXX',
10287 * ) );
10288 * ```
10289 *
10290 * @author Swashata Ghosh (@swashata)
10291 * @since 2.5.0
10292 *
10293 * @return bool True if we are running in bundle context, false otherwise.
10294 */
10295 private function has_bundle_context() {
10296 return ! is_null( $this->get_bundle_id() );
10297 }
10298
10299 /**
10300 * @author Vova Feldman (@svovaf)
10301 * @since 1.2.1.5
10302 *
10303 * @return string Freemius SDK version
10304 */
10305 function get_sdk_version() {
10306 return $this->version;
10307 }
10308
10309 /**
10310 * @author Vova Feldman (@svovaf)
10311 * @since 1.2.1.5
10312 *
10313 * @return number Parent plugin ID (if parent exist).
10314 */
10315 function get_parent_id() {
10316 return $this->is_addon() ?
10317 $this->get_parent_instance()->get_id() :
10318 $this->_plugin->id;
10319 }
10320
10321 /**
10322 * @author Vova Feldman (@svovaf)
10323 * @since 2.3.1
10324 *
10325 * @return string
10326 */
10327 function get_usage_tracking_terms_url() {
10328 return $this->apply_filters(
10329 'usage_tracking_terms_url',
10330 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10331 );
10332 }
10333
10334 /**
10335 * @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.
10336 * @since 2.5.8
10337 *
10338 * @return string
10339 */
10340 function get_license_activation_terms_url() {
10341 return $this->apply_filters(
10342 'license_activation_terms_url',
10343 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10344 );
10345 }
10346
10347 /**
10348 * @author Vova Feldman (@svovaf)
10349 * @since 2.3.1
10350 *
10351 * @return string
10352 */
10353 function get_eula_url() {
10354 return $this->apply_filters(
10355 'eula_url',
10356 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10357 );
10358 }
10359
10360 /**
10361 * @author Vova Feldman (@svovaf)
10362 * @since 1.0.1
10363 *
10364 * @return string Plugin public key.
10365 */
10366 function get_public_key() {
10367 return $this->_plugin->public_key;
10368 }
10369
10370 /**
10371 * Will be available only on sandbox mode.
10372 *
10373 * @author Vova Feldman (@svovaf)
10374 * @since 1.0.4
10375 *
10376 * @return mixed Plugin secret key.
10377 */
10378 function get_secret_key() {
10379 return $this->_plugin->secret_key;
10380 }
10381
10382 /**
10383 * @author Vova Feldman (@svovaf)
10384 * @since 1.1.1
10385 *
10386 * @return bool
10387 */
10388 function has_secret_key() {
10389 return ! empty( $this->_plugin->secret_key );
10390 }
10391
10392 /**
10393 * @author Vova Feldman (@svovaf)
10394 * @since 1.0.9
10395 *
10396 * @param string|bool $premium_suffix
10397 *
10398 * @return string
10399 */
10400 function get_plugin_name( $premium_suffix = false ) {
10401 $this->_logger->entrance();
10402
10403 /**
10404 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10405 *
10406 * @author Vova Feldman
10407 */
10408 if ( ! isset( $this->_plugin_name ) ) {
10409 // Name is not yet set.
10410 $this->set_name( $premium_suffix );
10411 } else if (
10412 ! empty( $premium_suffix ) &&
10413 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10414 ) {
10415 // Name is already set, but there's a change in the premium suffix.
10416 $this->set_name( $premium_suffix );
10417 }
10418
10419 return $this->_plugin_name;
10420 }
10421
10422 /**
10423 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10424 *
10425 * @author Vova Feldman (@svovaf)
10426 * @since 2.2.1
10427 *
10428 * @param string $premium_suffix
10429 */
10430 private function set_name( $premium_suffix = '' ) {
10431 $plugin_data = $this->get_plugin_data();
10432
10433 // Get name.
10434 $this->_plugin_name = $plugin_data['Name'];
10435
10436 if ( is_string( $premium_suffix ) ) {
10437 $premium_suffix = trim( $premium_suffix );
10438
10439 if ( ! empty( $premium_suffix ) ) {
10440 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10441 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10442 $suffix_len = strlen( $suffix );
10443
10444 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10445 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10446 ) {
10447 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10448 }
10449 }
10450 }
10451
10452 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10453 }
10454
10455 /**
10456 * @author Vova Feldman (@svovaf)
10457 * @since 1.0.0
10458 *
10459 * @param bool $reparse_plugin_metadata
10460 *
10461 * @return string
10462 */
10463 function get_plugin_version( $reparse_plugin_metadata = false ) {
10464 $this->_logger->entrance();
10465
10466 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10467
10468 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10469
10470 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10471 }
10472
10473 /**
10474 * @author Vova Feldman (@svovaf)
10475 * @since 1.2.1.7
10476 *
10477 * @return string
10478 */
10479 function get_plugin_title() {
10480 $this->_logger->entrance();
10481
10482 $title = $this->_plugin->title;
10483
10484 return $this->apply_filters( 'plugin_title', $title );
10485 }
10486
10487 /**
10488 * @author Vova Feldman (@svovaf)
10489 * @since 1.2.2.7
10490 *
10491 * @param bool $lowercase
10492 *
10493 * @return string
10494 */
10495 function get_module_label( $lowercase = false ) {
10496 $label = $this->is_addon() ?
10497 $this->get_text_inline( 'Add-On', 'addon' ) :
10498 ( $this->is_plugin() ?
10499 $this->get_text_inline( 'Plugin', 'plugin' ) :
10500 $this->get_text_inline( 'Theme', 'theme' ) );
10501
10502 if ( $lowercase ) {
10503 $label = strtolower( $label );
10504 }
10505
10506 return $label;
10507 }
10508
10509 /**
10510 * @author Vova Feldman (@svovaf)
10511 * @since 1.0.4
10512 *
10513 * @return string
10514 */
10515 function get_plugin_basename() {
10516 if ( ! isset( $this->_plugin_basename ) ) {
10517 if ( $this->is_plugin() ) {
10518 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10519 } else {
10520 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10521 }
10522 }
10523
10524 return $this->_plugin_basename;
10525 }
10526
10527 function get_plugin_folder_name() {
10528 $this->_logger->entrance();
10529
10530 $plugin_folder = $this->_plugin_basename;
10531
10532 while ( '.' !== dirname( $plugin_folder ) ) {
10533 $plugin_folder = dirname( $plugin_folder );
10534 }
10535
10536 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10537
10538 return $plugin_folder;
10539 }
10540
10541 #endregion ------------------------------------------------------------------
10542
10543 /* Account
10544 ------------------------------------------------------------------------------------------------------------------*/
10545
10546 /**
10547 * Find plugin's slug by plugin's basename.
10548 *
10549 * @author Vova Feldman (@svovaf)
10550 * @since 1.0.9
10551 *
10552 * @param string $plugin_base_name
10553 *
10554 * @return false|string
10555 */
10556 private static function find_slug_by_basename( $plugin_base_name ) {
10557 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10558
10559 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10560 return false;
10561 }
10562
10563 return $file_slug_map[ $plugin_base_name ];
10564 }
10565
10566 /**
10567 * Store the map between the plugin's basename to the slug.
10568 *
10569 * @author Vova Feldman (@svovaf)
10570 * @since 1.0.9
10571 */
10572 private function store_file_slug_map() {
10573 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10574
10575 if ( ! array( $file_slug_map ) ) {
10576 $file_slug_map = array();
10577 }
10578
10579 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10580 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10581 ) {
10582 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10583 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10584 }
10585 }
10586
10587 /**
10588 * @return array[number]FS_User
10589 */
10590 static function get_all_users() {
10591 $users = self::maybe_get_entities_account_option( 'users', array() );
10592
10593 if ( ! is_array( $users ) ) {
10594 $users = array();
10595 }
10596
10597 return $users;
10598 }
10599
10600 /**
10601 * @param string $module_type
10602 * @param null|int $blog_id Since 2.0.0
10603 *
10604 * @return array[string]FS_Site
10605 */
10606 private static function get_all_sites(
10607 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10608 $blog_id = null,
10609 $is_backup = false
10610 ) {
10611 $sites = self::get_account_option(
10612 ( $is_backup ? 'prev_' : '' ) . 'sites',
10613 $module_type,
10614 $blog_id
10615 );
10616
10617 if ( ! is_array( $sites ) ) {
10618 $sites = array();
10619 }
10620
10621 return $sites;
10622 }
10623
10624 /**
10625 * @author Leo Fajardo (@leorw)
10626 *
10627 * @since 1.2.2
10628 *
10629 * @param string $option_name
10630 * @param string $module_type
10631 * @param null|int $network_level_or_blog_id Since 2.0.0
10632 *
10633 * @return mixed
10634 */
10635 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10636 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10637 $option_name = $module_type . '_' . $option_name;
10638 }
10639
10640 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10641 }
10642
10643 /**
10644 * @author Leo Fajardo (@leorw)
10645 *
10646 * @since 1.2.2
10647 *
10648 * @param string $option_name
10649 * @param mixed $option_value
10650 * @param bool $store
10651 * @param null|int $network_level_or_blog_id Since 2.0.0
10652 */
10653 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10654 self::set_account_option_by_module(
10655 $this->_module_type,
10656 $option_name,
10657 $option_value,
10658 $store,
10659 $network_level_or_blog_id
10660 );
10661 }
10662
10663 /**
10664 * @author Vova Feldman (@svovaf)
10665 *
10666 * @since 1.2.2.7
10667 *
10668 * @param string $module_type
10669 * @param string $option_name
10670 * @param mixed $option_value
10671 * @param bool $store
10672 * @param null|int $network_level_or_blog_id Since 2.0.0
10673 */
10674 private static function set_account_option_by_module(
10675 $module_type,
10676 $option_name,
10677 $option_value,
10678 $store,
10679 $network_level_or_blog_id = null
10680 ) {
10681 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10682 $option_name = $module_type . '_' . $option_name;
10683 }
10684
10685 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10686 }
10687
10688 /**
10689 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10690 *
10691 * @author Leo Fajardo (@leorw)
10692 * @since 2.3.1
10693 *
10694 * @param string $option_name
10695 * @param mixed $default
10696 * @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).
10697 *
10698 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10699 */
10700 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10701 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10702
10703 $class_name = '';
10704
10705 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10706 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10707 }
10708
10709 switch ( $option_name ) {
10710 case 'plugins':
10711 case 'themes':
10712 case 'addons':
10713 $class_name = FS_Plugin::get_class_name();
10714 break;
10715 case 'users':
10716 $class_name = FS_User::get_class_name();
10717 break;
10718 case 'sites':
10719 $class_name = FS_Site::get_class_name();
10720 break;
10721 case 'licenses':
10722 case 'all_licenses':
10723 $class_name = FS_Plugin_License::get_class_name();
10724 break;
10725 case 'plans':
10726 $class_name = FS_Plugin_Plan::get_class_name();
10727 break;
10728 case 'updates':
10729 $class_name = FS_Plugin_Tag::get_class_name();
10730 break;
10731 }
10732
10733 if ( empty( $class_name ) ) {
10734 return $option;
10735 }
10736
10737 return fs_get_entities( $option, $class_name );
10738 }
10739
10740 /**
10741 * @author Vova Feldman (@svovaf)
10742 * @since 1.0.6
10743 *
10744 * @param number|null $module_id
10745 *
10746 * @return FS_Plugin_License[]
10747 */
10748 private static function get_all_licenses( $module_id = null ) {
10749 $licenses = self::get_account_option( 'all_licenses' );
10750
10751 if ( ! is_array( $licenses ) ) {
10752 $licenses = array();
10753 }
10754
10755 if ( is_null( $module_id ) ) {
10756 return $licenses;
10757 }
10758
10759 $licenses = isset( $licenses[ $module_id ] ) ?
10760 $licenses[ $module_id ] :
10761 array();
10762
10763 return $licenses;
10764 }
10765
10766 /**
10767 * @author Leo Fajardo (@leorw)
10768 * @since 2.0.0
10769 *
10770 * @return array
10771 */
10772 private static function get_all_licenses_by_module_type() {
10773 $licenses = self::get_account_option( 'all_licenses' );
10774
10775 $licenses_by_module_type = array(
10776 WP_FS__MODULE_TYPE_PLUGIN => array(),
10777 WP_FS__MODULE_TYPE_THEME => array()
10778 );
10779
10780 if ( ! is_array( $licenses ) ) {
10781 return $licenses_by_module_type;
10782 }
10783
10784 foreach ( $licenses as $module_id => $module_licenses ) {
10785 $fs = self::get_instance_by_id( $module_id );
10786 if ( false === $fs ) {
10787 continue;
10788 }
10789
10790 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10791 }
10792
10793 return $licenses_by_module_type;
10794 }
10795
10796 /**
10797 * @author Leo Fajardo (@leorw)
10798 * @since 2.0.0
10799 *
10800 * @param number $module_id
10801 * @param number|null $user_id
10802 *
10803 * @return array
10804 */
10805 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10806 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10807
10808 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10809 $all_modules_user_id_license_ids_map = array();
10810 }
10811
10812 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10813 $all_modules_user_id_license_ids_map[ $module_id ] :
10814 array();
10815
10816 if ( FS_User::is_valid_id( $user_id ) ) {
10817 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10818 $user_id_license_ids_map[ $user_id ] :
10819 array();
10820 }
10821
10822 return $user_id_license_ids_map;
10823 }
10824
10825 /**
10826 * @author Leo Fajardo (@leorw)
10827 * @since 2.0.0
10828 *
10829 * @param array $new_user_id_license_ids_map
10830 * @param number $module_id
10831 * @param number|null $user_id
10832 */
10833 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10834 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10835 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10836 $all_modules_user_id_license_ids_map = array();
10837 }
10838
10839 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10840 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10841 }
10842
10843 if ( FS_User::is_valid_id( $user_id ) ) {
10844 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10845 } else {
10846 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10847 }
10848
10849 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10850 }
10851
10852 /**
10853 * Get a collection of the user's linked license IDs.
10854 *
10855 * @author Vova Feldman (@svovaf)
10856 * @since 2.0.0
10857 *
10858 * @param number $user_id
10859 *
10860 * @return number[]
10861 */
10862 private function get_user_linked_license_ids( $user_id ) {
10863 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10864 }
10865
10866 /**
10867 * Override the user's linked license IDs with a new IDs collection.
10868 *
10869 * @author Vova Feldman (@svovaf)
10870 * @since 2.0.0
10871 *
10872 * @param number $user_id
10873 * @param number[] $license_ids
10874 */
10875 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10876 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10877 }
10878
10879 /**
10880 * Link a specified license ID to a given user.
10881 *
10882 * @author Vova Feldman (@svovaf)
10883 * @since 2.0.0
10884 *
10885 * @param number $license_id
10886 * @param number $user_id
10887 */
10888 private function link_license_2_user( $license_id, $user_id ) {
10889 $license_ids = $this->get_user_linked_license_ids( $user_id );
10890
10891 if ( in_array( $license_id, $license_ids ) ) {
10892 // License already linked.
10893 return;
10894 }
10895
10896 $license_ids[] = $license_id;
10897
10898 $this->set_user_linked_license_ids( $user_id, $license_ids );
10899 }
10900
10901 /**
10902 * @param string|bool $module_type
10903 *
10904 * @return FS_Plugin_Plan[]
10905 */
10906 private static function get_all_plans( $module_type = false ) {
10907 $plans = self::get_account_option( 'plans', $module_type );
10908
10909 if ( ! is_array( $plans ) ) {
10910 $plans = array();
10911 }
10912
10913 return $plans;
10914 }
10915
10916 /**
10917 * @author Vova Feldman (@svovaf)
10918 * @since 1.0.4
10919 *
10920 * @return FS_Plugin_Tag[]
10921 */
10922 private static function get_all_updates() {
10923 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10924
10925 if ( ! is_array( $updates ) ) {
10926 $updates = array();
10927 }
10928
10929 return $updates;
10930 }
10931
10932 /**
10933 * @author Vova Feldman (@svovaf)
10934 * @since 1.0.6
10935 *
10936 * @return array<number,FS_Plugin[]>|false
10937 */
10938 private static function get_all_addons() {
10939 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10940
10941 if ( ! is_array( $addons ) ) {
10942 $addons = array();
10943 }
10944
10945 return $addons;
10946 }
10947
10948 /**
10949 * @author Vova Feldman (@svovaf)
10950 * @since 1.0.6
10951 *
10952 * @return number[]|false
10953 */
10954 private static function get_all_account_addons() {
10955 $addons = self::$_accounts->get_option( 'account_addons', array() );
10956
10957 if ( ! is_array( $addons ) ) {
10958 $addons = array();
10959 }
10960
10961 return $addons;
10962 }
10963
10964 /**
10965 * Check if user has connected his account (opted-in).
10966 *
10967 * Note:
10968 * If the user opted-in and opted-out on a later stage,
10969 * this will still return true. If you want to check if the
10970 * user is currently opted-in, use:
10971 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10972 *
10973 * @author Vova Feldman (@svovaf)
10974 * @since 1.0.1
10975 *
10976 * @param bool $ignore_anonymous_state Since 2.5.1
10977 *
10978 * @return bool
10979 */
10980 function is_registered( $ignore_anonymous_state = false ) {
10981 return (
10982 is_object( $this->_user ) &&
10983 (
10984 $this->is_premium() ||
10985 $ignore_anonymous_state ||
10986 ! $this->is_anonymous()
10987 )
10988 );
10989 }
10990
10991 /**
10992 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10993 *
10994 * @author Leo Fajardo (@leorw)
10995 * @since 1.2.1.5
10996 *
10997 * @return bool
10998 */
10999 function is_tracking_allowed( $blog_id = null, $install = null ) {
11000 if ( is_null( $install ) ) {
11001 $install = is_null( $blog_id ) ?
11002 $this->_site :
11003 $this->get_install_by_blog_id( $blog_id );
11004 }
11005
11006 return (
11007 is_object( $install ) &&
11008 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
11009 );
11010 }
11011
11012 /**
11013 * Returns TRUE if the user never opted-in or manually opted-out.
11014 *
11015 * @author Vova Feldman (@svovaf)
11016 * @since 1.2.1.5
11017 *
11018 * @param int|null $blog_id
11019 *
11020 * @return bool
11021 */
11022 function is_tracking_prohibited( $blog_id = null ) {
11023 return (
11024 ! $this->is_registered( true ) ||
11025 ! $this->is_tracking_allowed( $blog_id )
11026 );
11027 }
11028
11029 /**
11030 * @author Leo Fajardo (@leorw)
11031 * @since 2.4.0
11032 *
11033 * @return bool
11034 */
11035 function is_bundle_license_auto_activation_enabled() {
11036 return $this->is_addon() ?
11037 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
11038 $this->_is_bundle_license_auto_activation_enabled;
11039 }
11040
11041 /**
11042 * @author Vova Feldman (@svovaf)
11043 * @since 1.0.4
11044 *
11045 * @return FS_Plugin
11046 */
11047 function get_plugin() {
11048 return $this->_plugin;
11049 }
11050
11051 /**
11052 * @author Vova Feldman (@svovaf)
11053 * @since 1.0.3
11054 *
11055 * @return FS_User
11056 */
11057 function get_user() {
11058 return $this->_user;
11059 }
11060
11061 /**
11062 * @author Vova Feldman (@svovaf)
11063 * @since 1.0.3
11064 *
11065 * @return FS_Site
11066 */
11067 function get_site() {
11068 return $this->_site;
11069 }
11070
11071 /**
11072 * @author Leo Fajardo (@leorw)
11073 * @since 2.5.0
11074 */
11075 function store_site( $site ) {
11076 $this->_site = $site;
11077 $this->_store_site( true );
11078 }
11079
11080 /**
11081 * 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).
11082 *
11083 * @author Leo Fajardo (@leorw)
11084 * @since 2.5.0
11085 */
11086 function delete_current_install( $back_up ) {
11087 // Back up and delete the unique ID.
11088 if ( $back_up ) {
11089 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
11090 }
11091
11092 self::$_accounts->set_option( 'unique_id', null );
11093
11094 if ( $back_up ) {
11095 // 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).
11096 $this->back_up_site();
11097 }
11098
11099 $this->_delete_site();
11100 $this->_site = null;
11101 }
11102
11103 /**
11104 * @author Leo Fajardo (@leorw)
11105 * @since 2.5.0
11106 */
11107 function restore_backup_site() {
11108 self::$_accounts->set_option(
11109 'unique_id',
11110 self::$_accounts->get_option( 'prev_unique_id' )
11111 );
11112
11113 $sites = self::get_all_sites( $this->_module_type, null, true );
11114 $this->store_site( clone $sites[ $this->_slug ] );
11115 }
11116
11117 /**
11118 * Get plugin add-ons.
11119 *
11120 * @author Vova Feldman (@svovaf)
11121 * @since 1.0.6
11122 *
11123 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
11124 *
11125 * @param bool $flush
11126 *
11127 * @return FS_Plugin[]|false
11128 */
11129 function get_addons( $flush = false ) {
11130 $this->_logger->entrance();
11131
11132 if ( ! $this->_has_addons ) {
11133 return false;
11134 }
11135
11136 $addons = $this->sync_addons( $flush );
11137
11138 return ( ! is_array( $addons ) || empty( $addons ) ) ?
11139 false :
11140 $addons;
11141 }
11142
11143 /**
11144 * @author Vova Feldman (@svovaf)
11145 * @since 1.0.6
11146 *
11147 * @return number[]|false
11148 */
11149 function get_account_addons() {
11150 $this->_logger->entrance();
11151
11152 $addons = self::get_all_account_addons();
11153
11154 if ( ! is_array( $addons ) ||
11155 ! isset( $addons[ $this->_plugin->id ] ) ||
11156 ! is_array( $addons[ $this->_plugin->id ] ) ||
11157 0 === count( $addons[ $this->_plugin->id ] )
11158 ) {
11159 return false;
11160 }
11161
11162 return $addons[ $this->_plugin->id ];
11163 }
11164
11165 /**
11166 * Check if user has any
11167 *
11168 * @author Vova Feldman (@svovaf)
11169 * @since 1.1.6
11170 *
11171 * @return bool
11172 */
11173 function has_account_addons() {
11174 $addons = $this->get_account_addons();
11175
11176 return is_array( $addons ) && ( 0 < count( $addons ) );
11177 }
11178
11179
11180 /**
11181 * Get add-on by ID (from local data).
11182 *
11183 * @author Vova Feldman (@svovaf)
11184 * @since 1.0.6
11185 *
11186 * @param number $id
11187 *
11188 * @return FS_Plugin|false
11189 */
11190 function get_addon( $id ) {
11191 $this->_logger->entrance();
11192
11193 $addons = $this->get_addons();
11194
11195 if ( is_array( $addons ) ) {
11196 foreach ( $addons as $addon ) {
11197 if ( $id == $addon->id ) {
11198 return $addon;
11199 }
11200 }
11201 }
11202
11203 return false;
11204 }
11205
11206 /**
11207 * Get add-on by slug (from local data).
11208 *
11209 * @author Vova Feldman (@svovaf)
11210 * @since 1.0.6
11211 *
11212 * @param string $slug
11213 *
11214 * @param bool $flush
11215 *
11216 * @return FS_Plugin|false
11217 */
11218 function get_addon_by_slug( $slug, $flush = false ) {
11219 $this->_logger->entrance();
11220
11221 $addons = $this->get_addons( $flush );
11222
11223 if ( is_array( $addons ) ) {
11224 foreach ( $addons as $addon ) {
11225 if ( $slug === $addon->slug ) {
11226 return $addon;
11227 }
11228 }
11229 }
11230
11231 return false;
11232 }
11233
11234 /**
11235 * @var array<number,object[]> {
11236 * @key number Add-on ID.
11237 * @val object[] The add-on's plans and prices object.
11238 * }
11239 */
11240 private $plans_and_pricing_by_addon_id;
11241
11242 /**
11243 * @author Leo Fajardo (@leorw)
11244 * @since 2.3.0
11245 *
11246 * @return array<number,object[]> {
11247 * @key number Add-on ID.
11248 * @val object[] The add-on's plans and prices object.
11249 * }
11250 */
11251 function _get_addons_plans_and_pricing_map_by_id() {
11252 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11253 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11254
11255 $plans_and_pricing_by_addon_id = array();
11256 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11257 foreach ( $result->addons as $addon ) {
11258 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11259 }
11260 }
11261
11262 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11263 }
11264
11265 return $this->plans_and_pricing_by_addon_id;
11266 }
11267
11268 /**
11269 * @author Leo Fajardo (@leorw)
11270 * @since 2.3.0
11271 *
11272 * @param number $addon_id
11273 * @param bool $is_installed
11274 *
11275 * @return array
11276 */
11277 function _get_addon_info( $addon_id, $is_installed ) {
11278 $addon = $this->get_addon( $addon_id );
11279
11280 if ( ! is_object( $addon ) ) {
11281 // Unexpected call.
11282 return array();
11283 }
11284
11285 $slug = $addon->slug;
11286
11287 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11288
11289 if ( ! fs_is_network_admin() ) {
11290 // Get blog-level activated installations.
11291 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11292 } else {
11293 $sites = null;
11294
11295 if ( $this->is_addon_activated( $addon_id ) &&
11296 $this->get_addon_instance( $addon_id )->is_network_active()
11297 ) {
11298 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11299 // Get network-level activated installations.
11300 $sites = self::maybe_get_entities_account_option(
11301 'sites',
11302 array(),
11303 $addon_storage->network_install_blog_id
11304 );
11305 }
11306 }
11307 }
11308
11309 $addon_info = array(
11310 'is_connected' => false,
11311 'slug' => $slug,
11312 'title' => $addon->title,
11313 'is_whitelabeled' => $addon_storage->is_whitelabeled
11314 );
11315
11316 if ( ! $is_installed ) {
11317 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11318
11319 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11320 $has_paid_plan = false;
11321 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11322
11323 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11324 foreach ( $plans as $plan ) {
11325 if ( isset( $plan->pricing ) &&
11326 is_array( $plan->pricing ) &&
11327 count( $plan->pricing ) > 0
11328 ) {
11329 $has_paid_plan = true;
11330 break;
11331 }
11332 }
11333 }
11334
11335 $addon_info['has_paid_plan'] = $has_paid_plan;
11336 }
11337 }
11338
11339 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11340 return $addon_info;
11341 }
11342
11343 $site = $sites[ $slug ];
11344
11345 $addon_info['is_connected'] = (
11346 ( $addon->parent_plugin_id == $this->get_id() ) &&
11347 is_object( $site ) &&
11348 FS_Site::is_valid_id( $site->id ) &&
11349 FS_User::is_valid_id( $site->user_id ) &&
11350 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11351 );
11352
11353 if ( $addon_info['is_connected'] && $is_installed ) {
11354 return $addon_info;
11355 }
11356
11357 $addon_info['site'] = $site;
11358
11359 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11360 if ( isset( $plugins_data[ $slug ] ) ) {
11361 $plugin_data = $plugins_data[ $slug ];
11362
11363 $addon_info['version'] = $plugin_data->version;
11364 }
11365
11366 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11367 if ( isset( $all_plans[ $slug ] ) ) {
11368 $plans = $all_plans[ $slug ];
11369
11370 foreach ( $plans as $plan ) {
11371 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11372 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11373 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11374 break;
11375 }
11376 }
11377 }
11378
11379 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11380 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11381 foreach ( $licenses[ $addon_id ] as $license ) {
11382 if ( $license->id == $site->license_id ) {
11383 $addon_info['license'] = $license;
11384 break;
11385 }
11386 }
11387 }
11388
11389 if ( isset( $addon_info['license'] ) ) {
11390 if ( isset( $addon_storage->subscriptions ) &&
11391 ! empty( $addon_storage->subscriptions )
11392 ) {
11393 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11394
11395 foreach ( $addon_subscriptions as $subscription ) {
11396 if ( $subscription->license_id == $site->license_id ) {
11397 $addon_info['subscription'] = $subscription;
11398 break;
11399 }
11400 }
11401 }
11402 }
11403
11404 return $addon_info;
11405 }
11406
11407 /**
11408 * @author Vova Feldman (@svovaf)
11409 * @since 2.0.0
11410 *
11411 * @param number $user_id
11412 *
11413 * @return FS_User
11414 */
11415 static function _get_user_by_id( $user_id ) {
11416 self::$_static_logger->entrance( "user_id = {$user_id}" );
11417
11418 $users = self::get_all_users();
11419
11420 if ( is_array( $users ) ) {
11421 if ( isset( $users[ $user_id ] ) &&
11422 $users[ $user_id ] instanceof FS_User &&
11423 $user_id == $users[ $user_id ]->id
11424 ) {
11425 return $users[ $user_id ];
11426 }
11427
11428 // If user wasn't found by the key, iterate over all the users collection.
11429 foreach ( $users as $user ) {
11430 /**
11431 * @var FS_User $user
11432 */
11433 if ( $user_id == $user->id ) {
11434 return $user;
11435 }
11436 }
11437 }
11438
11439 return null;
11440 }
11441
11442 /**
11443 * Checks if a Freemius user_id is associated with a super-admin.
11444 *
11445 * @author Vova Feldman (@svovaf)
11446 * @since 2.0.0
11447 *
11448 * @param number $user_id
11449 *
11450 * @return bool
11451 */
11452 private static function is_super_admin( $user_id ) {
11453 $is_super_admin = false;
11454
11455 $user = self::_get_user_by_id( $user_id );
11456
11457 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11458 self::require_pluggable_essentials();
11459
11460 $wp_user = get_user_by( 'email', $user->email );
11461
11462 if ( $wp_user instanceof WP_User ) {
11463 $super_admins = get_super_admins();
11464 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11465 }
11466 }
11467
11468 return $is_super_admin;
11469 }
11470
11471 #----------------------------------------------------------------------------------
11472 #region Plans & Licensing
11473 #----------------------------------------------------------------------------------
11474
11475 /**
11476 * Check if running premium plugin code.
11477 *
11478 * @author Vova Feldman (@svovaf)
11479 * @since 1.0.5
11480 *
11481 * @return bool
11482 */
11483 function is_premium() {
11484 /**
11485 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11486 * `register_constructor_hooks` method.
11487 *
11488 * @author Leo Fajardo (@leorw)
11489 * @since 2.2.3
11490 */
11491 return is_object( $this->_plugin ) ?
11492 $this->_plugin->is_premium :
11493 false;
11494 }
11495
11496 /**
11497 * Get site's plan ID.
11498 *
11499 * @author Vova Feldman (@svovaf)
11500 * @since 1.0.2
11501 *
11502 * @return number
11503 */
11504 function get_plan_id() {
11505 return $this->_site->plan_id;
11506 }
11507
11508 /**
11509 * Get site's plan title.
11510 *
11511 * @author Vova Feldman (@svovaf)
11512 * @since 1.0.2
11513 *
11514 * @return string
11515 */
11516 function get_plan_title() {
11517 $plan = $this->get_plan();
11518
11519 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11520 }
11521
11522 /**
11523 * Get site's plan name.
11524 *
11525 * @author Vova Feldman (@svovaf)
11526 * @since 2.0.0
11527 *
11528 * @return string
11529 */
11530 function get_plan_name() {
11531 $plan = $this->get_plan();
11532
11533 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11534 }
11535
11536 /**
11537 * @author Vova Feldman (@svovaf)
11538 * @since 1.0.9
11539 *
11540 * @return FS_Plugin_Plan|false
11541 */
11542 function get_plan() {
11543 if ( ! is_object( $this->_site ) ) {
11544 return false;
11545 }
11546
11547 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11548 $this->_get_plan_by_id( $this->_site->plan_id ) :
11549 false;
11550 }
11551
11552 /**
11553 * @author Vova Feldman (@svovaf)
11554 * @since 1.0.3
11555 *
11556 * @return bool
11557 */
11558 function is_trial() {
11559 $this->_logger->entrance();
11560
11561 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11562 return false;
11563 }
11564
11565 return $this->_site->is_trial();
11566 }
11567
11568 /**
11569 * Check if currently in a trial with payment method (credit card or paypal).
11570 *
11571 * @author Vova Feldman (@svovaf)
11572 * @since 1.1.7
11573 *
11574 * @return bool
11575 */
11576 function is_paid_trial() {
11577 $this->_logger->entrance();
11578
11579 if ( ! $this->is_trial() ) {
11580 return false;
11581 }
11582
11583 if ( ! $this->has_active_valid_license() ) {
11584 return false;
11585 }
11586
11587 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11588 return false;
11589 }
11590
11591 /**
11592 * @var FS_Subscription $subscription
11593 */
11594 $subscription = $this->_get_subscription( $this->_license->id );
11595
11596 return ( is_object( $subscription ) && $subscription->is_active() );
11597 }
11598
11599 /**
11600 * Check if trial already utilized.
11601 *
11602 * @since 1.0.9
11603 *
11604 * @return bool
11605 */
11606 function is_trial_utilized() {
11607 $this->_logger->entrance();
11608
11609 if ( ! $this->is_registered() ) {
11610 return false;
11611 }
11612
11613 return $this->_site->is_trial_utilized();
11614 }
11615
11616 /**
11617 * Get trial plan information (if in trial).
11618 *
11619 * @author Vova Feldman (@svovaf)
11620 * @since 1.0.9
11621 *
11622 * @return bool|FS_Plugin_Plan
11623 */
11624 function get_trial_plan() {
11625 $this->_logger->entrance();
11626
11627 if ( ! $this->is_trial() ) {
11628 return false;
11629 }
11630
11631 // Try to load plan from local cache.
11632 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11633
11634 if ( ! is_object( $trial_plan ) ) {
11635 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11636
11637 /**
11638 * If managed to fetch the plan, add it to the plans collection.
11639 */
11640 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11641 if ( ! is_array( $this->_plans ) ) {
11642 $this->_plans = array();
11643 }
11644
11645 $this->_plans[] = $trial_plan;
11646 $this->_store_plans();
11647 }
11648 }
11649
11650 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11651 return $trial_plan;
11652 }
11653
11654 /**
11655 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11656 */
11657 $trial_plan = new FS_Plugin_Plan();
11658 $trial_plan->id = $this->_site->trial_plan_id;
11659 $trial_plan->name = 'pro';
11660 $trial_plan->title = 'Pro';
11661
11662 return $trial_plan;
11663 }
11664
11665 /**
11666 * Check if the user has an activate, non-expired license on current plugin's install.
11667 *
11668 * @since 1.0.9
11669 *
11670 * @return bool
11671 */
11672 function is_paying() {
11673 $this->_logger->entrance();
11674
11675 if ( ! $this->is_registered( true ) ) {
11676 return false;
11677 }
11678
11679 if ( ! $this->has_paid_plan() ) {
11680 return false;
11681 }
11682
11683 return (
11684 ! $this->is_trial() &&
11685 'free' !== $this->get_plan_name() &&
11686 $this->has_active_valid_license()
11687 );
11688 }
11689
11690 /**
11691 * @author Vova Feldman (@svovaf)
11692 * @since 1.0.4
11693 *
11694 * @return bool
11695 */
11696 function is_free_plan() {
11697 if ( ! $this->is_registered() ) {
11698 return true;
11699 }
11700
11701 if ( ! $this->has_paid_plan() ) {
11702 return true;
11703 }
11704
11705 return (
11706 'free' === $this->get_plan_name() ||
11707 ! $this->has_features_enabled_license()
11708 );
11709 }
11710
11711 /**
11712 * @author Vova Feldman (@svovaf)
11713 * @since 1.0.5
11714 *
11715 * @return bool
11716 */
11717 function _has_premium_license() {
11718 $this->_logger->entrance();
11719
11720 $premium_license = $this->_get_available_premium_license();
11721
11722 return ( false !== $premium_license );
11723 }
11724
11725 /**
11726 * Check if user has any licenses associated with the plugin (including expired or blocking).
11727 *
11728 * @author Vova Feldman (@svovaf)
11729 * @since 1.1.7.3
11730 *
11731 * @param bool $including_foreign
11732 *
11733 * @return bool
11734 */
11735 function has_any_license( $including_foreign = true ) {
11736 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11737 return false;
11738 }
11739
11740 if ( $including_foreign ) {
11741 return true;
11742 }
11743
11744 foreach ( $this->_licenses as $license ) {
11745 if ( $this->_user->id == $license->user_id ) {
11746 return true;
11747 }
11748 }
11749
11750 return false;
11751 }
11752
11753 /**
11754 * @author Vova Feldman (@svovaf)
11755 * @since 1.0.5
11756 *
11757 * @param bool|null $is_localhost
11758 *
11759 * @return FS_Plugin_License|false
11760 */
11761 function _get_available_premium_license( $is_localhost = null ) {
11762 $this->_logger->entrance();
11763
11764 $licenses = $this->get_available_premium_licenses( $is_localhost );
11765 if ( ! empty( $licenses ) ) {
11766 return $licenses[0];
11767 }
11768
11769 return false;
11770 }
11771
11772 /**
11773 * @author Vova Feldman (@svovaf)
11774 * @since 1.0.5
11775 *
11776 * @param bool|null $is_localhost
11777 *
11778 * @return FS_Plugin_License[]
11779 */
11780 function get_available_premium_licenses( $is_localhost = null ) {
11781 $this->_logger->entrance();
11782
11783 $licenses = array();
11784 if ( ! $this->has_paid_plan() ) {
11785 return $licenses;
11786 }
11787
11788 if ( is_array( $this->_licenses ) ) {
11789 foreach ( $this->_licenses as $license ) {
11790 if ( ! $license->can_activate( $is_localhost ) ) {
11791 continue;
11792 }
11793
11794 $licenses[] = $license;
11795 }
11796 }
11797
11798 return $licenses;
11799 }
11800
11801 /**
11802 * Sync local plugin plans with remote server.
11803 *
11804 * 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).
11805 *
11806 * @author Vova Feldman (@svovaf)
11807 * @since 1.0.5
11808 *
11809 * @return FS_Plugin_Plan[]|object
11810 */
11811 function _sync_plans() {
11812 $plans = $this->_fetch_plugin_plans();
11813
11814 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11815 $plans_map = array();
11816 foreach ( $plans as $plan ) {
11817 $plans_map[ $plan->id ] = true;
11818 }
11819
11820 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11821
11822 foreach ( $plans_ids_to_keep as $plan_id ) {
11823 if ( isset( $plans_map[ $plan_id ] ) ) {
11824 continue;
11825 }
11826
11827 $missing_plan = self::_get_plan_by_id( $plan_id );
11828
11829 if ( is_object( $missing_plan ) ) {
11830 $plans[] = $missing_plan;
11831 }
11832 }
11833
11834 $this->_plans = $plans;
11835 $this->_store_plans();
11836 }
11837
11838 $this->do_action( 'after_plans_sync', $plans );
11839
11840 return $this->_plans;
11841 }
11842
11843 /**
11844 * Check if specified plan exists locally. If not, fetch it and store it.
11845 *
11846 * @author Vova Feldman (@svovaf)
11847 * @since 2.0.0
11848 *
11849 * @param number $plan_id
11850 *
11851 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11852 */
11853 private function sync_plan_if_not_exist( $plan_id ) {
11854 $plan = self::_get_plan_by_id( $plan_id );
11855
11856 if ( is_object( $plan ) ) {
11857 // Plan already exists.
11858 return $plan;
11859 }
11860
11861 $plan = $this->fetch_plan_by_id( $plan_id );
11862
11863 if ( $plan instanceof FS_Plugin_Plan ) {
11864 $this->_plans[] = $plan;
11865 $this->_store_plans();
11866
11867 return $plan;
11868 }
11869
11870 return $plan;
11871 }
11872
11873 /**
11874 * Check if specified license exists locally. If not, fetch it and store it.
11875 *
11876 * @author Vova Feldman (@svovaf)
11877 * @since 2.0.0
11878 *
11879 * @param number $license_id
11880 * @param string $license_key
11881 *
11882 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11883 */
11884 private function sync_license_if_not_exist( $license_id, $license_key ) {
11885 $license = $this->_get_license_by_id( $license_id );
11886
11887 if ( is_object( $license ) ) {
11888 // License already exists.
11889 return $license;
11890 }
11891
11892 $license = $this->fetch_license_by_key( $license_id, $license_key );
11893
11894 if ( $license instanceof FS_Plugin_License ) {
11895 $this->_licenses[] = $license;
11896
11897 $this->set_license( $license );
11898
11899 $this->_store_licenses();
11900
11901 return $license;
11902 }
11903
11904 return $license;
11905 }
11906
11907 /**
11908 * Get a collection of unique plan IDs that are associated with any installs in the network.
11909 *
11910 * @author Leo Fajardo (@leorw)
11911 * @since 2.0.0
11912 *
11913 * @return number[]
11914 */
11915 private function get_plans_ids_associated_with_installs() {
11916 if ( ! is_multisite() ) {
11917 if ( ! is_object( $this->_site ) ||
11918 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11919 ) {
11920 return array();
11921 }
11922
11923 return array( $this->_site->plan_id );
11924 }
11925
11926 $plan_ids = array();
11927 $sites = self::get_sites();
11928 foreach ( $sites as $site ) {
11929 $blog_id = self::get_site_blog_id( $site );
11930 $install = $this->get_install_by_blog_id( $blog_id );
11931
11932 if ( ! is_object( $install ) ||
11933 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11934 ) {
11935 continue;
11936 }
11937
11938 $plan_ids[ $install->plan_id ] = true;
11939 }
11940
11941 return array_keys( $plan_ids );
11942 }
11943
11944 /**
11945 * Get a collection of unique license IDs that are associated with any installs in the network.
11946 *
11947 * @author Leo Fajardo (@leorw)
11948 * @since 2.0.0
11949 *
11950 * @return number[]
11951 */
11952 private function get_license_ids_associated_with_installs() {
11953 if ( ! $this->_is_network_active ) {
11954 if ( ! is_object( $this->_site ) ||
11955 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11956 ) {
11957 return array();
11958 }
11959
11960 return array( $this->_site->license_id );
11961 }
11962
11963 $license_ids = array();
11964 $sites = self::get_sites();
11965 foreach ( $sites as $site ) {
11966 $blog_id = self::get_site_blog_id( $site );
11967 $install = $this->get_install_by_blog_id( $blog_id );
11968
11969 if ( ! is_object( $install ) ||
11970 ! FS_Plugin_License::is_valid_id( $install->license_id )
11971 ) {
11972 continue;
11973 }
11974
11975 $license_ids[ $install->license_id ] = true;
11976 }
11977
11978 return array_keys( $license_ids );
11979 }
11980
11981 /**
11982 * @author Vova Feldman (@svovaf)
11983 * @since 1.0.5
11984 *
11985 * @param number $id
11986 *
11987 * @return FS_Plugin_Plan|false
11988 */
11989 function _get_plan_by_id( $id ) {
11990 $this->_logger->entrance();
11991
11992 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11993 $this->_sync_plans();
11994 }
11995
11996 foreach ( $this->_plans as $plan ) {
11997 if ( $id == $plan->id ) {
11998 return $plan;
11999 }
12000 }
12001
12002 return false;
12003 }
12004
12005 /**
12006 * @author Vova Feldman (@svovaf)
12007 * @since 1.1.8.1
12008 *
12009 * @param string $name
12010 *
12011 * @return FS_Plugin_Plan|false
12012 */
12013 private function get_plan_by_name( $name ) {
12014 $this->_logger->entrance();
12015
12016 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12017 $this->_sync_plans();
12018 }
12019
12020 foreach ( $this->_plans as $plan ) {
12021 if ( $name == $plan->name ) {
12022 return $plan;
12023 }
12024 }
12025
12026 return false;
12027 }
12028
12029 /**
12030 * Sync local licenses with remote server.
12031 *
12032 * @author Vova Feldman (@svovaf)
12033 * @since 1.0.6
12034 *
12035 * @param number|bool $site_license_id
12036 * @param number|null $blog_id
12037 *
12038 * @return FS_Plugin_License[]|object
12039 */
12040 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
12041 $this->_logger->entrance();
12042
12043 $is_network_admin = fs_is_network_admin();
12044
12045 if ( $is_network_admin && is_null( $blog_id ) ) {
12046 $all_licenses = self::get_all_licenses( $this->_module_id );
12047 } else {
12048 $all_licenses = $this->get_user_licenses( $this->_user->id );
12049 }
12050
12051 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
12052
12053 $all_licenses_map = array();
12054 foreach ( $all_licenses as $license ) {
12055 $all_licenses_map[ $license->id ] = true;
12056 }
12057
12058 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
12059
12060 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
12061 $licenses_map = array();
12062 foreach ( $licenses as $license ) {
12063 $licenses_map[ $license->id ] = true;
12064 }
12065
12066 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
12067 // foreach ( $license_ids_to_keep as $license_id ) {
12068 // if ( isset( $licenses_map[ $license_id ] ) ) {
12069 // continue;
12070 // }
12071 //
12072 // $missing_license = self::_get_license_by_id( $license_id, false );
12073 // if ( is_object( $missing_license ) ) {
12074 // $licenses[] = $missing_license;
12075 // $licenses_map[ $missing_license->id ] = true;
12076 // }
12077 // }
12078
12079 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
12080
12081 foreach ( $user_license_ids as $key => $license_id ) {
12082 if ( ! isset( $licenses_map[ $license_id ] ) ) {
12083 // Remove access to licenses that no longer exist.
12084 unset( $user_license_ids[ $key ] );
12085 }
12086 }
12087
12088 if ( ! empty( $user_license_ids ) ) {
12089 foreach ( $licenses_map as $license_id => $value ) {
12090 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
12091 // Associate new licenses with the user who triggered the license syncing.
12092 $user_license_ids[] = $license_id;
12093 }
12094 }
12095
12096 $user_license_ids = array_unique( $user_license_ids );
12097 } else {
12098 $user_license_ids = array_keys( $licenses_map );
12099 }
12100
12101 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
12102 $user_licenses = array();
12103 foreach ( $licenses as $license ) {
12104 if ( ! in_array( $license->id, $user_license_ids ) ) {
12105 continue;
12106 }
12107
12108 $user_licenses[] = $license;
12109 }
12110
12111 $this->_licenses = $user_licenses;
12112 } else {
12113 $this->_licenses = $licenses;
12114 }
12115
12116 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
12117
12118 $this->_store_licenses( true, $this->_module_id, $licenses );
12119 }
12120
12121 // Update current license.
12122 if ( is_object( $this->_license ) ) {
12123 $license = $this->_get_license_by_id( $this->_license->id );
12124
12125 if ( is_object( $license ) ) {
12126 /**
12127 * `$license` can be `false` in case a user change action has just been completed and this method
12128 * has synced the `$this->_licenses` collection for the new user. In this case, the
12129 * `$this->_licenses` collection may have only the newly activated license that is associated with
12130 * the new user. `set_license` will eventually be called in the same request by the logic that
12131 * follows outside this method which will detect that the install's license has been updated, and
12132 * then `_update_site_license` will be called which in turn will call `set_license`.
12133 *
12134 * @author Leo Fajardo (@leorw)
12135 * @since 2.3.2
12136 */
12137 $this->set_license( $license );
12138 }
12139 }
12140
12141 return $this->_licenses;
12142 }
12143
12144 /**
12145 * @author Vova Feldman (@svovaf)
12146 * @since 1.0.5
12147 *
12148 * @param number $id
12149 * @param bool $sync_licenses
12150 *
12151 * @return FS_Plugin_License|false
12152 */
12153 function _get_license_by_id( $id, $sync_licenses = true ) {
12154 $this->_logger->entrance();
12155
12156 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
12157 return false;
12158 }
12159
12160 /**
12161 * When running from the network level admin and opted-in from the network,
12162 * check if the license exists in the network user licenses collection.
12163 *
12164 * @author Vova Feldman (@svovaf)
12165 * @since 2.0.0
12166 */
12167 if ( fs_is_network_admin() &&
12168 $this->is_network_registered() &&
12169 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
12170 ) {
12171 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
12172
12173 foreach ( $licenses as $license ) {
12174 if ( $id == $license->id ) {
12175 return $license;
12176 }
12177 }
12178 }
12179
12180 if ( ! $this->has_any_license() && $sync_licenses ) {
12181 $this->_sync_licenses( $id );
12182 }
12183
12184 if ( is_array( $this->_licenses ) ) {
12185 foreach ( $this->_licenses as $license ) {
12186 if ( $id == $license->id ) {
12187 return $license;
12188 }
12189 }
12190 }
12191
12192 return false;
12193 }
12194
12195 /**
12196 * 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.
12197 *
12198 * @author Vova Feldman (@svovaf)
12199 * @since 2.0.0
12200 *
12201 * @param number $id
12202 *
12203 * @return FS_Plugin_License
12204 */
12205 private function get_license_by_id( $id ) {
12206 $licenses = self::get_all_licenses( $this->_module_id );
12207
12208 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
12209 foreach ( $licenses as $license ) {
12210 if ( $id == $license->id ) {
12211 return $license;
12212 }
12213 }
12214 }
12215
12216 return null;
12217 }
12218
12219 /**
12220 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
12221 *
12222 * @author Vova Feldman (@svovaf)
12223 * @since 2.0.0
12224 *
12225 * @return \FS_Plugin_License|mixed
12226 */
12227 private function sync_site_license() {
12228 $api = $this->get_api_user_scope();
12229
12230 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
12231
12232 if ( ! $this->is_api_result_entity( $result ) ) {
12233 return $result;
12234 }
12235
12236 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12237 $this->_store_licenses();
12238
12239 return $license;
12240 }
12241
12242 /**
12243 * Get all user's available licenses for the current module.
12244 *
12245 * @author Vova Feldman (@svovaf)
12246 * @since 2.0.0
12247 *
12248 * @param number $user_id
12249 *
12250 * @return FS_Plugin_License[]
12251 */
12252 private function get_user_licenses( $user_id ) {
12253 $all_licenses = self::get_all_licenses( $this->_module_id );
12254 if ( empty( $all_licenses ) ) {
12255 return array();
12256 }
12257
12258 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12259 if ( empty( $user_license_ids ) ) {
12260 return array();
12261 }
12262
12263 $licenses = array();
12264 foreach ( $all_licenses as $license ) {
12265 if ( in_array( $license->id, $user_license_ids ) ) {
12266 $licenses[] = $license;
12267 }
12268 }
12269
12270 return $licenses;
12271 }
12272
12273 /**
12274 * Checks if the context license is network activated except on the given blog ID.
12275 *
12276 * @author Vova Feldman (@svovaf)
12277 * @since 2.0.0
12278 *
12279 * @param int $except_blog_id
12280 *
12281 * @return bool
12282 */
12283 private function is_license_network_active( $except_blog_id = 0 ) {
12284 $this->_logger->entrance();
12285
12286 if ( ! is_object( $this->_license ) ) {
12287 return false;
12288 }
12289
12290 $sites = self::get_sites();
12291
12292 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12293 // There are more sites than the number of activations, so license cannot be network activated.
12294 return false;
12295 }
12296
12297 foreach ( $sites as $site ) {
12298 $blog_id = self::get_site_blog_id( $site );
12299
12300 if ( $except_blog_id == $blog_id ) {
12301 // Skip excluded blog.
12302 continue;
12303 }
12304
12305 $install = $this->get_install_by_blog_id( $blog_id );
12306
12307 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12308 return false;
12309 }
12310 }
12311
12312 return true;
12313 }
12314
12315 /**
12316 * 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.
12317 *
12318 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12319 *
12320 * @author Vova Feldman (@svovaf)
12321 * @since 2.0.0
12322 *
12323 * @param \FS_User $user
12324 * @param \FS_Plugin_License $license
12325 *
12326 * @return bool
12327 */
12328 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12329 $this->_logger->entrance();
12330
12331 $result = $this->can_activate_license_on_network( $license );
12332
12333 if ( false === $result ) {
12334 return false;
12335 }
12336
12337 $installs_without_license = $result['installs'];
12338 if ( ! empty( $installs_without_license ) ) {
12339 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12340 }
12341
12342 $disconnected_site_ids = $result['sites'];
12343 if ( ! empty( $disconnected_site_ids ) ) {
12344 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12345 }
12346
12347 $this->link_license_2_user( $license->id, $user->id );
12348
12349 // Sync license after activations.
12350 $license->activated += $result['production_count'];
12351 $license->activated_local += $result['localhost_count'];
12352
12353 // $this->_store_licenses()
12354
12355 return true;
12356 }
12357
12358 /**
12359 * Checks if the given license can be activated on the whole network.
12360 *
12361 * @author Vova Feldman (@svovaf)
12362 * @since 2.0.0
12363 *
12364 * @param \FS_Plugin_License $license
12365 *
12366 * @return false|array {
12367 * @type array[int]FS_Site $installs Blog ID to install map.
12368 * @type int[] $sites Non-connected blog IDs.
12369 * @type int $production_count Production sites count.
12370 * @type int $localhost_count Production sites count.
12371 * }
12372 */
12373 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12374 $sites = self::get_sites();
12375
12376 $production_count = 0;
12377 $localhost_count = 0;
12378
12379 $installs_without_license = array();
12380 $disconnected_site_ids = array();
12381
12382 foreach ( $sites as $site ) {
12383 $blog_id = self::get_site_blog_id( $site );
12384 $install = $this->get_install_by_blog_id( $blog_id );
12385
12386 if ( is_object( $install ) ) {
12387 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12388 // License already activated on the install.
12389 continue;
12390 }
12391
12392 $url = $install->url;
12393
12394 $installs_without_license[ $blog_id ] = $install;
12395 } else {
12396 $url = is_object( $site ) ?
12397 $site->siteurl :
12398 self::get_unfiltered_site_url( $blog_id );
12399
12400 $disconnected_site_ids[] = $blog_id;
12401 }
12402
12403 if ( FS_Site::is_localhost_by_address( $url ) ) {
12404 $localhost_count ++;
12405 } else {
12406 $production_count ++;
12407 }
12408 }
12409
12410 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12411 return false;
12412 }
12413
12414 return array(
12415 'installs' => $installs_without_license,
12416 'sites' => $disconnected_site_ids,
12417 'production_count' => $production_count,
12418 'localhost_count' => $localhost_count,
12419 );
12420 }
12421
12422 /**
12423 * Activate a given license on a collection of installs.
12424 *
12425 * @author Vova Feldman (@svovaf)
12426 * @since 2.0.0
12427 *
12428 * @param \FS_User $user
12429 * @param string $license_key
12430 * @param array $blog_2_install_map {
12431 * @key int Blog ID.
12432 * @value FS_Site Blog's associated install.
12433 * }
12434 *
12435 * @return mixed|true
12436 */
12437 private function activate_license_on_many_installs(
12438 FS_User $user,
12439 $license_key,
12440 array $blog_2_install_map
12441 ) {
12442 $params = array(
12443 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12444 );
12445
12446 $install_2_blog_map = array();
12447 foreach ( $blog_2_install_map as $blog_id => $install ) {
12448 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12449
12450 $install_2_blog_map[ $install->id ] = $blog_id;
12451 }
12452
12453 $result = $this->get_api_user_scope_by_user( $user )->call(
12454 "plugins/{$this->_plugin->id}/installs.json",
12455 'PUT',
12456 $params
12457 );
12458
12459 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12460 return $result;
12461 }
12462
12463 foreach ( $result->installs as $r_install ) {
12464 $install = new FS_Site( $r_install );
12465 $install->is_disconnected = false;
12466
12467 // Update install.
12468 $this->_store_site(
12469 true,
12470 $install_2_blog_map[ $r_install->id ],
12471 $install
12472 );
12473 }
12474
12475 return true;
12476 }
12477
12478 /**
12479 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12480 *
12481 * @author Vova Feldman (@svovaf)
12482 * @since 2.3.1
12483 *
12484 * @param \FS_User $user
12485 * @param string $license_key
12486 *
12487 * @return true|mixed True if successful, otherwise, the API result.
12488 */
12489 private function activate_license_on_site( FS_User $user, $license_key ) {
12490 return $this->activate_license_on_many_sites( $user, $license_key );
12491 }
12492
12493 /**
12494 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12495 *
12496 * @author Vova Feldman (@svovaf)
12497 * @since 2.0.0
12498 *
12499 * @param \FS_User $user
12500 * @param string $license_key
12501 * @param int[] $site_ids
12502 *
12503 * @return true|mixed True if successful, otherwise, the API result.
12504 */
12505 private function activate_license_on_many_sites(
12506 FS_User $user,
12507 $license_key,
12508 array $site_ids = array()
12509 ) {
12510 $sites = array();
12511 foreach ( $site_ids as $site_id ) {
12512 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12513 }
12514
12515 // Install the plugin.
12516 $result = $this->create_installs_with_user(
12517 $user,
12518 $license_key,
12519 false,
12520 $sites,
12521 false,
12522 true
12523 );
12524
12525 if ( ! $this->is_api_result_entity( $result ) &&
12526 ! $this->is_api_result_object( $result, 'installs' )
12527 ) {
12528 return $result;
12529 }
12530
12531 $installs = array();
12532
12533 if ( $this->is_api_result_entity( $result ) ) {
12534 $install = new FS_Site( $result );
12535
12536 $this->_user = $user;
12537
12538 $this->_store_site( true, null, $install );
12539
12540 $this->_site = $install;
12541
12542 $this->reset_anonymous_mode();
12543 } else {
12544 foreach ( $result->installs as $install ) {
12545 $installs[] = new FS_Site( $install );
12546 }
12547
12548 // Map site addresses to their blog IDs.
12549 $address_to_blog_map = $this->get_address_to_blog_map();
12550
12551 $first_blog_id = null;
12552
12553 foreach ( $installs as $install ) {
12554 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12555 $blog_id = $address_to_blog_map[ $address ];
12556
12557 $this->_store_site( true, $blog_id, $install );
12558
12559 $this->reset_anonymous_mode( $blog_id );
12560
12561 if ( is_null( $first_blog_id ) ) {
12562 $first_blog_id = $blog_id;
12563 }
12564 }
12565
12566 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12567 $this->_storage->network_install_blog_id = $first_blog_id;
12568 }
12569 }
12570
12571 return true;
12572 }
12573
12574 /**
12575 * Sync site's license with user licenses.
12576 *
12577 * @author Vova Feldman (@svovaf)
12578 * @since 1.0.6
12579 *
12580 * @param FS_Plugin_License|null $new_license
12581 *
12582 * @return FS_Plugin_License|null
12583 */
12584 function _update_site_license( $new_license ) {
12585 $this->_logger->entrance();
12586
12587 /**
12588 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12589 * accordingly so that it will also handle the case when an ownership change is done via license
12590 * activation.
12591 *
12592 * @author Leo Fajardo (@leorw)
12593 * @since 2.3.2
12594 */
12595 $this->set_license( $new_license );
12596
12597 if ( ! is_object( $new_license ) ) {
12598 $this->_site->license_id = null;
12599 $this->_sync_site_subscription( null );
12600
12601 return $this->_license;
12602 }
12603
12604 $this->_site->license_id = $this->_license->id;
12605
12606 if ( ! is_array( $this->_licenses ) ) {
12607 $this->_licenses = array();
12608 }
12609
12610 $is_license_found = false;
12611 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12612 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12613 $this->_licenses[ $i ] = $new_license;
12614
12615 $is_license_found = true;
12616 break;
12617 }
12618 }
12619
12620 // If new license just append.
12621 if ( ! $is_license_found ) {
12622 $this->_licenses[] = $new_license;
12623 }
12624
12625 $this->_sync_site_subscription( $new_license );
12626
12627 return $this->_license;
12628 }
12629
12630 /**
12631 * @author Vova Feldman (@svovaf)
12632 * @since 2.3.1
12633 *
12634 * @param \FS_Plugin_License $license
12635 */
12636 private function set_license( FS_Plugin_License $license = null ) {
12637 $this->_license = $license;
12638
12639 $this->maybe_update_whitelabel_flag( $license );
12640 }
12641
12642 /**
12643 * @author Leo Fajardo (@leorw)
12644 * @since 2.3.1
12645 *
12646 * @param FS_Plugin_License $license
12647 */
12648 private function maybe_update_whitelabel_flag( $license ) {
12649 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12650 $this->_storage->is_whitelabeled :
12651 false;
12652
12653 if ( is_object( $license ) ) {
12654 $license_user = self::_get_user_by_id( $license->user_id );
12655
12656 if ( ! is_object( $license_user ) ) {
12657 // If foreign license, do not update the `is_whitelabeled` flag.
12658 return;
12659 }
12660
12661 if ( $this->is_addon() ) {
12662 /**
12663 * Store the last license data to the parent's storage since it's needed only when showing the
12664 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12665 * iterate over the add-ons just to get the last license data.
12666 */
12667 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12668 } else {
12669 $this->store_last_activated_license_data( $license );
12670 }
12671
12672 if ( $license->is_whitelabeled ) {
12673 // Activated a developer license, data should be hidden.
12674 $is_whitelabeled = true;
12675 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12676 // The account owner activated a regular license key, no need to hide the data.
12677 $is_whitelabeled = false;
12678 }
12679 }
12680
12681 $this->_storage->is_whitelabeled = $is_whitelabeled;
12682
12683 // Reset the whitelabeled status after update.
12684 $this->is_whitelabeled = null;
12685 if ( $this->is_addon() ) {
12686 $parent_fs = $this->get_parent_instance();
12687
12688 if ( is_object( $parent_fs ) ) {
12689 $parent_fs->is_whitelabeled = null;
12690 }
12691 }
12692 }
12693
12694 /**
12695 * @author Leo Fajardo (@leorw)
12696 * @since 2.3.1
12697 *
12698 * @param FS_Plugin_License $license
12699 * @param FS_User $license_user
12700 */
12701 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12702 if ( ! is_object( $license_user ) ) {
12703 $this->_storage->last_license_key = md5( $license->secret_key );
12704 $this->_storage->last_license_user_id = null;
12705 } else {
12706 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12707 $this->_storage->last_license_user_id = $license_user->id;
12708 }
12709 }
12710
12711 /**
12712 * @author Leo Fajardo (@leorw)
12713 * @since 2.3.1
12714 *
12715 * @param bool $ignore_data_debug_mode
12716 *
12717 * @return bool
12718 */
12719 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12720 if ( true !== $this->_storage->is_whitelabeled ) {
12721 return false;
12722 } else if ( $ignore_data_debug_mode ) {
12723 return true;
12724 }
12725
12726 $fs = $this->is_addon() ?
12727 $this->get_parent_instance() :
12728 $this;
12729
12730 return ! $fs->is_data_debug_mode();
12731 }
12732
12733 /**
12734 * @author Leo Fajardo (@leorw)
12735 * @since 2.3.1
12736 *
12737 * @return number
12738 */
12739 function get_last_license_user_id() {
12740 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12741 $this->_storage->last_license_user_id :
12742 null;
12743 }
12744
12745 /**
12746 * @author Leo Fajardo (@leorw)
12747 * @since 2.3.1
12748 *
12749 * @param int $blog_id
12750 * @param bool $ignore_data_debug_mode
12751 *
12752 * @return bool
12753 */
12754 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12755 if ( ! is_null( $blog_id ) ) {
12756 $this->switch_to_blog( $blog_id );
12757 }
12758
12759 if ( ! is_null( $this->is_whitelabeled ) ) {
12760 $is_whitelabeled = $this->is_whitelabeled;
12761 } else {
12762 $is_whitelabeled = false;
12763
12764 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12765
12766 if ( ! $this->has_addons() ) {
12767 $is_whitelabeled = $is_whitelabeled_flag;
12768 } else if ( $is_whitelabeled_flag ) {
12769 $is_whitelabeled = true;
12770 } else {
12771 if ( $this->is_registered() || $this->is_premium() ) {
12772 $addon_ids = $this->get_updated_account_addons();
12773 } else {
12774 $addons = self::get_all_addons();
12775
12776 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12777 $addons[ $this->_plugin->id ] :
12778 array();
12779
12780 $addon_ids = array();
12781 foreach ( $plugin_addons as $addon ) {
12782 $addon_ids[] = $addon->id;
12783 }
12784 }
12785
12786 $installed_addons = $this->get_installed_addons();
12787 foreach ( $installed_addons as $fs_addon ) {
12788 $addon_ids[] = $fs_addon->get_id();
12789 }
12790
12791 if ( ! empty( $addon_ids ) ) {
12792 $addon_ids = array_unique( $addon_ids );
12793
12794 $is_network_level = (
12795 fs_is_network_admin() &&
12796 $this->is_network_active()
12797 );
12798
12799 foreach ( $addon_ids as $addon_id ) {
12800 $addon = $this->get_addon( $addon_id );
12801
12802 if ( ! is_object( $addon ) ) {
12803 continue;
12804 }
12805
12806 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12807 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12808 self::get_addon_instance( $addon_id ) :
12809 null;
12810
12811 $was_addon_network_activated = false;
12812
12813 if ( is_object( $fs_addon ) ) {
12814 $was_addon_network_activated = $fs_addon->is_network_active();
12815 } else if ( $is_network_level ) {
12816 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12817 }
12818
12819 $network_delegated_connection = (
12820 $was_addon_network_activated &&
12821 $addon_storage->get( 'is_delegated_connection', false, true )
12822 );
12823
12824 if (
12825 $is_network_level &&
12826 ( ! $was_addon_network_activated || $network_delegated_connection )
12827 ) {
12828 $sites = self::get_sites();
12829
12830 /**
12831 * If in network admin area and the add-on was not network-activated or network-activated
12832 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12833 */
12834 foreach ( $sites as $site ) {
12835 $site_info = $this->get_site_info( $site );
12836
12837 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12838 $is_whitelabeled = true;
12839 break;
12840 }
12841 }
12842
12843 if ( $is_whitelabeled ) {
12844 break;
12845 }
12846 } else {
12847 /**
12848 * This will be executed when any of the following is met:
12849 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12850 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12851 * 3. Add-on was not network-activated and in site admin area.
12852 */
12853 if ( true === $addon_storage->is_whitelabeled ) {
12854 $is_whitelabeled = true;
12855 break;
12856 }
12857 }
12858 }
12859 }
12860 }
12861
12862 $this->is_whitelabeled = $is_whitelabeled;
12863
12864 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12865 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12866 }
12867
12868 if ( ! is_null( $blog_id ) ) {
12869 $this->restore_current_blog();
12870 }
12871 }
12872
12873 return (
12874 $is_whitelabeled &&
12875 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12876 );
12877 }
12878
12879 /**
12880 * Sync site's subscription.
12881 *
12882 * @author Vova Feldman (@svovaf)
12883 * @since 1.0.9
12884 *
12885 * @param FS_Plugin_License|null $license
12886 *
12887 * @return bool|\FS_Subscription
12888 */
12889 private function _sync_site_subscription( $license ) {
12890 if ( ! is_object( $license ) ) {
12891 $this->delete_unused_subscriptions();
12892
12893 return false;
12894 }
12895
12896 // Load subscription details if not lifetime.
12897 $subscription = $license->is_lifetime() ?
12898 false :
12899 $this->_fetch_site_license_subscription();
12900
12901 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12902 $this->store_subscription( $subscription );
12903 } else {
12904 $this->delete_unused_subscriptions();
12905 }
12906
12907 return $subscription;
12908 }
12909
12910 /**
12911 * @author Vova Feldman (@svovaf)
12912 * @since 1.0.6
12913 *
12914 * @return bool|\FS_Plugin_License
12915 */
12916 function _get_license() {
12917 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12918 return $this->_license;
12919 }
12920
12921 return $this->_get_available_premium_license();
12922 }
12923
12924 /**
12925 * @param number $license_id
12926 *
12927 * @return null|\FS_Subscription
12928 */
12929 function _get_subscription( $license_id ) {
12930 if ( ! isset( $this->_storage->subscriptions ) ||
12931 empty( $this->_storage->subscriptions )
12932 ) {
12933 return null;
12934 }
12935
12936 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12937 if ( $subscription->license_id == $license_id ) {
12938 return $subscription;
12939 }
12940 }
12941
12942 return null;
12943 }
12944
12945 /**
12946 * @author Leo Fajardo (@leorw)
12947 * @since 2.0.0
12948 *
12949 * @param FS_Subscription $subscription
12950 */
12951 function store_subscription( FS_Subscription $subscription ) {
12952 if ( ! isset( $this->_storage->subscriptions ) ) {
12953 $this->_storage->subscriptions = array();
12954 }
12955
12956 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12957 $this->_storage->subscriptions = array( $subscription );
12958
12959 return;
12960 }
12961
12962 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12963
12964 $updated_subscription = false;
12965 foreach ( $subscriptions as $key => $existing_subscription ) {
12966 if ( $existing_subscription->id == $subscription->id ) {
12967 $subscriptions[ $key ] = $subscription;
12968 $updated_subscription = true;
12969 break;
12970 }
12971 }
12972
12973 if ( ! $updated_subscription ) {
12974 $subscriptions[] = $subscription;
12975 }
12976
12977 $this->_storage->subscriptions = $subscriptions;
12978 }
12979
12980 /**
12981 * @author Leo Fajardo (@leorw)
12982 * @since 2.0.0
12983 */
12984 function delete_unused_subscriptions() {
12985 if ( ! isset( $this->_storage->subscriptions ) ||
12986 empty( $this->_storage->subscriptions ) ||
12987 // Clean up only if there are already at least 3 subscriptions.
12988 ( count( $this->_storage->subscriptions ) < 3 )
12989 ) {
12990 return;
12991 }
12992
12993 if ( ! is_multisite() ) {
12994 // If not multisite, there should only be 1 subscription, so just clear the array.
12995 $this->_storage->subscriptions = array();
12996
12997 return;
12998 }
12999
13000 $subscriptions_to_keep_by_license_id_map = array();
13001 $sites = self::get_sites();
13002 foreach ( $sites as $site ) {
13003 $blog_id = self::get_site_blog_id( $site );
13004 $install = $this->get_install_by_blog_id( $blog_id );
13005
13006 if ( ! is_object( $install ) ||
13007 ! FS_Plugin_License::is_valid_id( $install->license_id )
13008 ) {
13009 continue;
13010 }
13011
13012 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
13013 }
13014
13015 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
13016 $this->_storage->subscriptions = array();
13017
13018 return;
13019 }
13020
13021 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
13022 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
13023 unset( $this->_storage->subscriptions[ $key ] );
13024 }
13025 }
13026 }
13027
13028 /**
13029 * @author Vova Feldman (@svovaf)
13030 * @since 1.0.2
13031 *
13032 * @param string $plan Plan name
13033 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13034 *
13035 * @return bool
13036 */
13037 function is_plan( $plan, $exact = false ) {
13038 $this->_logger->entrance();
13039
13040 if ( ! $this->is_registered() ) {
13041 return false;
13042 }
13043
13044 $plan = strtolower( $plan );
13045
13046 $current_plan_name = $this->get_plan_name();
13047
13048 if ( $current_plan_name === $plan ) {
13049 // Exact plan.
13050 return true;
13051 } else if ( $exact ) {
13052 // Required exact, but plans are different.
13053 return false;
13054 }
13055
13056 $current_plan_order = - 1;
13057 $required_plan_order = PHP_INT_MAX;
13058 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13059 if ( $plan === $this->_plans[ $i ]->name ) {
13060 $required_plan_order = $i;
13061 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
13062 $current_plan_order = $i;
13063 }
13064 }
13065
13066 return ( $current_plan_order > $required_plan_order );
13067 }
13068
13069 /**
13070 * Check if module has only one plan.
13071 *
13072 * @author Vova Feldman (@svovaf)
13073 * @since 1.2.1.7
13074 *
13075 * @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.
13076 *
13077 * @return bool
13078 */
13079 function is_single_plan( $double_check = false ) {
13080 $this->_logger->entrance();
13081
13082 if ( ! $this->is_registered() ||
13083 ! is_array( $this->_plans ) ||
13084 0 === count( $this->_plans )
13085 ) {
13086 return true;
13087 }
13088
13089 $has_free_plan = $this->has_free_plan();
13090
13091 if ( ! $has_free_plan && $double_check ) {
13092 foreach ( $this->_plans as $plan ) {
13093 if ( $plan->is_free() ) {
13094 $has_free_plan = true;
13095 break;
13096 }
13097 }
13098 }
13099
13100 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
13101 }
13102
13103 /**
13104 * Check if plan based on trial. If not in trial mode, should return false.
13105 *
13106 * @since 1.0.9
13107 *
13108 * @param string $plan Plan name
13109 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13110 *
13111 * @return bool
13112 */
13113 function is_trial_plan( $plan, $exact = false ) {
13114 $this->_logger->entrance();
13115
13116 if ( ! $this->is_registered() ) {
13117 return false;
13118 }
13119
13120 if ( ! $this->is_trial() ) {
13121 return false;
13122 }
13123
13124 $trial_plan = $this->get_trial_plan();
13125
13126 if ( $trial_plan->name === $plan ) {
13127 // Exact plan.
13128 return true;
13129 } else if ( $exact ) {
13130 // Required exact, but plans are different.
13131 return false;
13132 }
13133
13134 $current_plan_order = - 1;
13135 $required_plan_order = - 1;
13136 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13137 if ( $plan === $this->_plans[ $i ]->name ) {
13138 $required_plan_order = $i;
13139 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
13140 $current_plan_order = $i;
13141 }
13142 }
13143
13144 return ( $current_plan_order > $required_plan_order );
13145 }
13146
13147 /**
13148 * Check if plugin has any paid plans.
13149 *
13150 * @author Vova Feldman (@svovaf)
13151 * @since 1.0.7
13152 *
13153 * @return bool
13154 */
13155 function has_paid_plan() {
13156 return $this->_has_paid_plans ||
13157 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
13158 }
13159
13160 /**
13161 * Check if plugin has any plan with a trail.
13162 *
13163 * @author Vova Feldman (@svovaf)
13164 * @since 1.0.9
13165 *
13166 * @return bool
13167 */
13168 function has_trial_plan() {
13169 /**
13170 * @author Vova Feldman(@svovaf)
13171 * @since 1.2.1.5
13172 *
13173 * Allow setting a trial from the SDK without calling the API.
13174 * But, if the user did opt-in, continue using the real data from the API.
13175 */
13176 if ( $this->_trial_days >= 0 ) {
13177 return true;
13178 }
13179
13180 return $this->_storage->get( 'has_trial_plan', false );
13181 }
13182
13183 /**
13184 * Check if plugin has any free plan, or is it premium only.
13185 *
13186 * Note: If no plans configured, assume plugin is free.
13187 *
13188 * @author Vova Feldman (@svovaf)
13189 * @since 1.0.7
13190 *
13191 * @return bool
13192 */
13193 function has_free_plan() {
13194 return ! $this->is_only_premium();
13195 }
13196
13197 /**
13198 * Displays a license activation dialog box when the user clicks on the "Activate License"
13199 * or "Change License" link on the plugins
13200 * page.
13201 *
13202 * @author Leo Fajardo (@leorw)
13203 * @since 1.1.9
13204 */
13205 function _add_license_activation_dialog_box() {
13206 $vars = array(
13207 'id' => $this->_module_id,
13208 );
13209
13210 fs_require_template( 'forms/license-activation.php', $vars );
13211 fs_require_template( 'forms/resend-key.php', $vars );
13212 }
13213
13214 /**
13215 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
13216 *
13217 * @author Leo Fajardo (@leorw)
13218 * @since 2.5.0
13219 */
13220 function _add_email_address_update_dialog_box() {
13221 $vars = array( 'id' => $this->_module_id );
13222
13223 fs_require_template( 'forms/email-address-update.php', $vars );
13224 }
13225
13226 /**
13227 * @author Leo Fajardo (@leorw)
13228 * @since 2.5.0
13229 */
13230 function _add_email_address_update_option() {
13231 if ( ! $this->should_handle_user_change() ) {
13232 return;
13233 }
13234
13235 // Add email address update AJAX handler.
13236 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13237 }
13238
13239 /**
13240 * @author Leo Fajardo (@leorw)
13241 * @since 2.5.0
13242 */
13243 function _email_address_update_ajax_handler() {
13244 $this->check_ajax_referer( 'update_email_address' );
13245
13246 $new_email_address = fs_request_get( 'email_address' );
13247 $transfer_type = fs_request_get( 'transfer_type' );
13248
13249 $result = $this->update_email( $new_email_address );
13250
13251 if ( ! FS_Api::is_api_error( $result ) ) {
13252 self::shoot_ajax_success();
13253 }
13254
13255 $error = '';
13256
13257 if ( FS_Api::is_api_error_object( $result ) ) {
13258 switch ( $result->error->code ) {
13259 case 'user_exist':
13260 case 'account_verification_required':
13261 $error = array(
13262 'code' => 'change_ownership',
13263 'url' => $this->get_account_url( 'change_owner', array(
13264 'state' => 'init',
13265 'candidate_email' => $new_email_address,
13266 'transfer_type' => $transfer_type,
13267 ) ),
13268 );
13269
13270 break;
13271 }
13272 }
13273
13274 if ( empty( $error ) ) {
13275 $error = is_object( $result ) ?
13276 var_export( $result->error, true ) :
13277 $result;
13278 }
13279
13280 self::shoot_ajax_failure( $error );
13281 }
13282
13283 /**
13284 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13285 *
13286 * @author Leo Fajardo (@leorw)
13287 * @since 2.3.2
13288 *
13289 * @return number[]
13290 */
13291 function get_installs_ids_with_foreign_licenses() {
13292 $installs = array();
13293
13294 if (
13295 is_object( $this->_license ) &&
13296 $this->_site->user_id != $this->_license->user_id
13297 ) {
13298 $installs[] = $this->_site->id;
13299 }
13300
13301 /**
13302 * Also try to get foreign licenses for the context product's add-ons.
13303 */
13304 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13305
13306 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13307 if ( $slug == $this->get_slug() ) {
13308 continue;
13309 }
13310
13311 $install = $install_info['install'];
13312 $license = $install_info['license'];
13313
13314 if (
13315 is_object( $license ) &&
13316 $install->user_id != $license->user_id
13317 ) {
13318 $installs[] = $install->id;
13319 }
13320 }
13321
13322 return $installs;
13323 }
13324
13325 /**
13326 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13327 *
13328 * @author Leo Fajardo (@leorw)
13329 * @since 2.3.2
13330 *
13331 * @param number[] $install_ids
13332 */
13333 function _add_user_change_dialog_box( $install_ids ) {
13334 $vars = array(
13335 'id' => $this->_module_id,
13336 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13337 );
13338
13339 fs_require_template( 'forms/user-change.php', $vars );
13340 }
13341
13342 /**
13343 * @author Leo Fajardo (@leorw)
13344 * @since 2.3.1
13345 */
13346 function _add_data_debug_mode_dialog_box() {
13347 $vars = array(
13348 'id' => $this->_module_id,
13349 );
13350
13351 fs_require_template( 'forms/data-debug-mode.php', $vars );
13352 }
13353
13354 /**
13355 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13356 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13357 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13358 * is only activated on a single production site.
13359 *
13360 * @author Leo Fajardo (@leorw)
13361 * @since 2.2.1
13362 *
13363 * @param bool $is_license_deactivation
13364 *
13365 * @return array
13366 */
13367 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13368 if ( fs_is_network_admin() ) {
13369 // Subscription cancellation dialog box is currently not supported for multisite networks.
13370 return array();
13371 }
13372
13373 if ( $this->is_whitelabeled() ) {
13374 return array();
13375 }
13376
13377 $license = $this->_get_license();
13378
13379 /**
13380 * 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.
13381 *
13382 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13383 * @since 2.2.1
13384 */
13385 if ( ! is_object( $license ) ||
13386 $license->is_lifetime() ||
13387 ( ! $license->is_single_site() && $license->activated > 1 )
13388 ) {
13389 return array();
13390 }
13391
13392 /**
13393 * @var FS_Subscription $subscription
13394 */
13395 $subscription = $this->_get_subscription( $license->id );
13396 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13397 return array();
13398 }
13399
13400 return array(
13401 'id' => $this->_module_id,
13402 'license' => $license,
13403 'has_trial' => $this->is_paid_trial(),
13404 'is_license_deactivation' => $is_license_deactivation,
13405 );
13406 }
13407
13408 /**
13409 * @author Leo Fajardo (@leorw)
13410 * @since 2.0.2
13411 */
13412 function _add_premium_version_upgrade_selection_dialog_box() {
13413 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13414 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13415 return;
13416 }
13417
13418 $vars = array(
13419 'id' => $this->_module_id,
13420 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13421 $modules_update->response[ $this->_plugin_basename ]->new_version :
13422 $modules_update->response[ $this->_plugin_basename ]['new_version']
13423 );
13424
13425 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13426 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13427 }
13428
13429 /**
13430 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13431 * page.
13432 *
13433 * @author Leo Fajardo (@leorw)
13434 * @since 1.2.1.5
13435 */
13436 function _add_optout_dialog() {
13437 if ( $this->is_theme() ) {
13438 $vars = null;
13439 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13440 }
13441
13442 $vars = array( 'id' => $this->_module_id );
13443 fs_require_template( 'forms/optout.php', $vars );
13444 }
13445
13446 /**
13447 * Prepare page to include all required UI and logic for the license activation dialog.
13448 *
13449 * @author Vova Feldman (@svovaf)
13450 * @since 1.2.0
13451 */
13452 function _add_license_activation() {
13453 if ( $this->is_migration() ) {
13454 return;
13455 }
13456
13457 if ( ! $this->is_user_admin() ) {
13458 // Only admins can activate a license.
13459 return;
13460 }
13461
13462 if ( ! $this->has_paid_plan() ) {
13463 // Module doesn't have any paid plans.
13464 return;
13465 }
13466
13467 if (
13468 $this->has_premium_version() &&
13469 ! $this->is_premium() &&
13470 /**
13471 * Also handle the case when an upgrade was made using the free version.
13472 *
13473 * @author Leo Fajardo (@leorw)
13474 * @since 2.3.2
13475 */
13476 ! is_object( $this->_get_license() )
13477 ) {
13478 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13479 return;
13480 }
13481
13482 // Add license activation link and AJAX request handler.
13483 if ( self::is_plugins_page() ) {
13484 $is_network_admin = fs_is_network_admin();
13485
13486 if (
13487 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13488 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13489 ) {
13490 if (
13491 $this->is_premium() ||
13492 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13493 ) {
13494 /**
13495 * @since 1.2.0 Add license action link only on plugins page.
13496 */
13497 $this->_add_license_action_link();
13498 }
13499 }
13500 }
13501
13502 // Add license activation AJAX callback.
13503 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13504
13505 // Add resend license AJAX callback.
13506 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13507 }
13508
13509 /**
13510 * Prepares page to include all required UI and logic for the "Change User" dialog.
13511 *
13512 * @author Leo Fajardo (@leorw)
13513 * @since 2.3.2
13514 */
13515 function _add_user_change_option() {
13516 if ( ! $this->should_handle_user_change() ) {
13517 return;
13518 }
13519
13520 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13521
13522 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13523 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13524 return;
13525 }
13526
13527 // Add user change AJAX handler.
13528 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13529 }
13530
13531 /**
13532 * @author Leo Fajardo (@leorw)
13533 * @since 2.3.2
13534 */
13535 function should_handle_user_change() {
13536 if ( ! $this->is_user_admin() ) {
13537 // Only admins can change user.
13538 return false;
13539 }
13540
13541 if ( $this->is_addon() ) {
13542 return false;
13543 }
13544
13545 if ( ! $this->is_registered() ) {
13546 return false;
13547 }
13548
13549 if (
13550 $this->is_network_active() &&
13551 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13552 ) {
13553 // Handle only on site-level "Account" section for now.
13554 return false;
13555 }
13556
13557 return true;
13558 }
13559
13560 /**
13561 * @author Leo Fajardo (@leorw)
13562 * @since 2.0.2
13563 */
13564 function _add_premium_version_upgrade_selection() {
13565 if ( ! $this->is_user_admin() ) {
13566 return;
13567 }
13568
13569 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13570 // This is relevant only to the free versions and premium versions without an active license.
13571 return;
13572 }
13573
13574 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13575 $this->_add_premium_version_upgrade_selection_action();
13576 }
13577 }
13578
13579 /**
13580 * @author Edgar Melkonyan
13581 * @since 2.4.1
13582 *
13583 * @throws Freemius_Exception
13584 */
13585 function _toggle_whitelabel_mode_ajax_handler() {
13586 $this->_logger->entrance();
13587
13588 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13589
13590 if ( ! $this->is_user_admin() ) {
13591 // Only for admins.
13592 self::shoot_ajax_failure();
13593 }
13594
13595 $license = $this->get_api_user_scope()->call(
13596 "/licenses/{$this->_site->license_id}.json",
13597 'put',
13598 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13599 );
13600
13601 if ( ! $this->is_api_result_entity( $license ) ) {
13602 self::shoot_ajax_failure(
13603 FS_Api::is_api_error_object( $license ) ?
13604 $license->error->message :
13605 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13606 );
13607 }
13608
13609 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13610 $this->_store_licenses();
13611
13612 $this->_sync_license();
13613
13614 if ( ! $license->is_whitelabeled ) {
13615 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13616 } else {
13617 $this->_admin_notices->add_sticky(
13618 sprintf(
13619 $this->get_text_inline(
13620 '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.',
13621 'license_whitelabeled'
13622 ),
13623 "<strong>{$this->get_plugin_title()}</strong>",
13624 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13625 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13626 ),
13627 'license_whitelabeled'
13628 );
13629 }
13630
13631 self::shoot_ajax_response( array( 'success' => true ) );
13632 }
13633
13634 /**
13635 * @author Leo Fajardo (@leorw)
13636 * @since 2.3.0
13637 */
13638 function _add_beta_mode_update_handler() {
13639 if ( ! $this->is_user_admin() ) {
13640 return;
13641 }
13642
13643 if ( ! $this->is_premium() ) {
13644 return;
13645 }
13646
13647 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13648 }
13649
13650 /**
13651 * @author Leo Fajardo (@leorw)
13652 * @since 2.3.0
13653 */
13654 function _set_beta_mode_ajax_handler() {
13655 $this->_logger->entrance();
13656
13657 $this->check_ajax_referer( 'set_beta_mode' );
13658
13659 if ( ! $this->is_user_admin() ) {
13660 // Only for admins.
13661 self::shoot_ajax_failure();
13662 }
13663
13664 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13665
13666 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13667 self::shoot_ajax_failure();
13668 }
13669
13670 $site = $this->api_site_call(
13671 '',
13672 'put',
13673 array(
13674 'is_beta' => ( 'true' == $is_beta ),
13675 'fields' => 'is_beta'
13676 )
13677 );
13678
13679 if ( ! $this->is_api_result_entity( $site ) ) {
13680 self::shoot_ajax_failure(
13681 FS_Api::is_api_error_object( $site ) ?
13682 $site->error->message :
13683 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13684 );
13685 }
13686
13687 $this->_site->is_beta = $site->is_beta;
13688 $this->_store_site();
13689
13690 self::shoot_ajax_response( array( 'success' => true ) );
13691 }
13692
13693 /**
13694 * License activation WP AJAX handler.
13695 *
13696 * @author Leo Fajardo (@leorw)
13697 * @since 1.1.9
13698 *
13699 * @uses Freemius::activate_license()
13700 */
13701 function _activate_license_ajax_action() {
13702 $this->_logger->entrance();
13703
13704 $this->check_ajax_referer( 'activate_license' );
13705
13706 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13707
13708 if ( empty( $license_key ) ) {
13709 exit;
13710 }
13711
13712 $sites = fs_is_network_admin() ?
13713 fs_request_get( 'sites', array(), 'post' ) :
13714 array();
13715
13716 $result = $this->activate_license(
13717 $license_key,
13718 $sites,
13719 fs_request_get_bool( 'is_marketing_allowed', null ),
13720 fs_request_get( 'blog_id', null ),
13721 fs_request_get( 'module_id', null, 'post' ),
13722 fs_request_get( 'user_id', null ),
13723 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13724 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13725 );
13726
13727 if (
13728 $result['success'] &&
13729 $this->is_bundle_license_auto_activation_enabled()
13730 ) {
13731 $license = new FS_Plugin_License();
13732 $license->secret_key = $license_key;
13733
13734 $this->maybe_activate_bundle_license( $license, $sites );
13735 }
13736
13737 echo json_encode( $result );
13738
13739 exit;
13740 }
13741
13742 /**
13743 * User change WP AJAX handler.
13744 *
13745 * @author Leo Fajardo (@leorw)
13746 * @since 2.3.2
13747 */
13748 function _user_change_ajax_action() {
13749 $this->_logger->entrance();
13750
13751 $this->check_ajax_referer( 'change_user' );
13752
13753 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13754 $new_user_id = fs_request_get( 'user_id' );
13755
13756 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13757 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13758 }
13759
13760 $params = array();
13761
13762 if ( ! empty( $new_email_address ) ) {
13763 $params['user_email'] = $new_email_address;
13764 } else {
13765 $params['user_id'] = $new_user_id;
13766 }
13767
13768 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13769 $install_ids = array();
13770
13771 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13772 $install_ids[ $slug ] = $install_info['install']->id;
13773 }
13774
13775 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13776
13777 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13778
13779 if ( FS_Api::is_api_error( $install ) ) {
13780 $error = '';
13781
13782 if ( is_object( $install ) ) {
13783 switch ( $install->error->code ) {
13784 case 'user_exist':
13785 $error = (
13786 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13787 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13788 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>' ) .
13789 sprintf(
13790 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13791 $this->get_account_url( 'change_owner', array(
13792 'state' => 'init',
13793 'candidate_email' => $new_email_address
13794 ) ),
13795 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13796 )
13797 );
13798 break;
13799 }
13800 }
13801
13802 if ( empty( $error ) ) {
13803 $error = FS_Api::is_api_error_object( $install ) ?
13804 $install->error->message :
13805 var_export( $install->error, true );
13806 }
13807
13808 self::shoot_ajax_failure( $error );
13809 } else {
13810 if (
13811 // If successful ownership change.
13812 $this->get_user()->id != $install->user_id ||
13813 ! empty( $new_email_address )
13814 ) {
13815 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13816 }
13817 }
13818
13819 self::shoot_ajax_success();
13820 }
13821
13822 /**
13823 * @author Leo Fajardo (@leorw)
13824 * @since 2.3.2.14
13825 */
13826 function starting_migration() {
13827 if ( ! empty( $this->_storage->license_migration ) ) {
13828 // Do not overwrite the data if already set.
13829 return;
13830 }
13831
13832 $this->_storage->license_migration = array(
13833 'is_migrating' => true,
13834 'start_timestamp' => time()
13835 );
13836 }
13837
13838 /**
13839 * @author Leo Fajardo (@leorw)
13840 * @since 2.3.2.14
13841 */
13842 function is_migration() {
13843 if ( $this->is_addon() ) {
13844 return $this->get_parent_instance()->is_migration();
13845 }
13846
13847 if ( empty( $this->_storage->license_migration ) ) {
13848 return false;
13849 }
13850
13851 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13852 return false;
13853 }
13854
13855 return (
13856 // Return `true` if the migration is within 5 minutes from the starting time.
13857 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13858 );
13859 }
13860
13861 /**
13862 *
13863 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13864 *
13865 * @author Vova Feldman (@svovaf)
13866 * @since 2.3.0
13867 *
13868 * @param string $license_key
13869 * @param null|bool $is_marketing_allowed
13870 * @param null|number $plugin_id
13871 * @param array $sites
13872 * @param int $blog_id
13873 *
13874 * @return array {
13875 * @var bool $success
13876 * @var string $error
13877 * @var string $next_page
13878 * }
13879 *
13880 * @uses Freemius::activate_license()
13881 */
13882 function activate_migrated_license(
13883 $license_key,
13884 $is_marketing_allowed = null,
13885 $plugin_id = null,
13886 $sites = array(),
13887 $blog_id = null
13888 ) {
13889 $this->_logger->entrance();
13890
13891 $result = $this->activate_license(
13892 $license_key,
13893 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13894 $this->get_sites_for_network_level_optin() :
13895 $sites,
13896 $is_marketing_allowed,
13897 $blog_id,
13898 $plugin_id
13899 );
13900
13901 // No need to show the sticky after license activation notice after migrating a license.
13902 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13903
13904 return $result;
13905 }
13906
13907 /**
13908 * @author Leo Fajardo (@leorw)
13909 * @since 2.3.1
13910 *
13911 * @return string
13912 */
13913 function get_pricing_js_path() {
13914 if ( ! isset( $this->_pricing_js_path ) ) {
13915 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
13916
13917 if ( empty( $pricing_js_path ) ) {
13918 global $fs_active_plugins;
13919
13920 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13921 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13922 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13923
13924 $pricing_js_path = $plugin_or_theme_root_dir
13925 . '/'
13926 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13927 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13928 . '/includes/freemius-pricing/freemius-pricing.js';
13929
13930 break;
13931 }
13932 }
13933 }
13934
13935 $this->_pricing_js_path = $pricing_js_path;
13936 }
13937
13938 return $this->_pricing_js_path;
13939 }
13940
13941 /**
13942 * @author Leo Fajardo (@leorw)
13943 * @since 2.3.1
13944 *
13945 * @return bool
13946 */
13947 function should_use_external_pricing() {
13948 if ( is_null( $this->_use_external_pricing ) ) {
13949 $pricing_js_path = $this->get_pricing_js_path();
13950
13951 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
13952 }
13953
13954 return $this->_use_external_pricing;
13955 }
13956
13957 /**
13958 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13959 *
13960 * @author Vova Feldman (@svovaf)
13961 * @since 2.2.4
13962 * @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).
13963 * @param string $license_key
13964 * @param array $sites
13965 * @param null|bool $is_marketing_allowed
13966 * @param null|int $blog_id
13967 * @param null|number $plugin_id
13968 * @param null|number $license_owner_id
13969 * @param bool|null $is_extensions_tracking_allowed
13970 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13971 *
13972 *
13973 * @return array {
13974 * @var bool $success
13975 * @var string $error
13976 * @var string $next_page
13977 * }
13978 */
13979 private function activate_license(
13980 $license_key,
13981 $sites = array(),
13982 $is_marketing_allowed = null,
13983 $blog_id = null,
13984 $plugin_id = null,
13985 $license_owner_id = null,
13986 $is_extensions_tracking_allowed = null,
13987 $is_diagnostic_tracking_allowed = null
13988 ) {
13989 $this->_logger->entrance();
13990
13991 $license_key = trim( $license_key );
13992
13993 $is_network_activation_or_migration = (
13994 fs_is_network_admin() ||
13995 ( ! empty( $sites ) && $this->is_migration() )
13996 );
13997
13998 if ( ! $is_network_activation_or_migration ) {
13999 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
14000 $sites = array();
14001 }
14002
14003 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
14004 $this :
14005 $this->get_addon_instance( $plugin_id );
14006
14007 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
14008 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
14009 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
14010 ) );
14011
14012 $error = false;
14013 $next_page = false;
14014
14015 $has_valid_blog_id = is_numeric( $blog_id );
14016
14017 $user = null;
14018
14019 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
14020 /**
14021 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
14022 *
14023 * @author Vova Feldman (@svovaf)
14024 */
14025 $user = $fs->get_parent_instance()->get_current_or_network_user();
14026 } else if ( $fs->is_registered() ) {
14027 $user = $fs->get_current_or_network_user();
14028 }
14029
14030 if ( $has_valid_blog_id ) {
14031 /**
14032 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
14033 *
14034 * @author Leo Fajardo (@leorw)
14035 */
14036 $fs->switch_to_blog( $blog_id );
14037 }
14038
14039 if ( is_object( $user ) ) {
14040 $result = true;
14041
14042 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
14043 // If no specific blog ID was provided, activate the license for all sites in the network.
14044 $blog_2_install_map = array();
14045 $site_ids = array();
14046
14047 foreach ( $sites as $site ) {
14048 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
14049 continue;
14050 }
14051
14052 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
14053
14054 if ( is_object( $install ) ) {
14055 $blog_2_install_map[ $site['blog_id'] ] = $install;
14056 } else {
14057 $site_ids[] = $site['blog_id'];
14058 }
14059 }
14060
14061 if ( ! empty( $blog_2_install_map ) ) {
14062 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
14063 }
14064
14065 if ( true === $result && ! empty( $site_ids ) ) {
14066 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
14067 }
14068 } else {
14069 if ( $fs->is_registered() ) {
14070 $params = array(
14071 'license_key' => $fs->apply_filters( 'license_key', $license_key )
14072 );
14073
14074 $install_ids = array();
14075
14076 $change_owner = FS_User::is_valid_id( $license_owner_id );
14077
14078 if ( $change_owner ) {
14079 $params['user_id'] = $license_owner_id;
14080
14081 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
14082
14083 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
14084 $install_ids[ $slug ] = $install_info['install']->id;
14085 }
14086
14087 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
14088 }
14089
14090 $api = $fs->get_api_site_scope();
14091
14092 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
14093
14094 if ( ! FS_Api::is_api_error( $result ) ) {
14095 $install = $result;
14096
14097 $fs->reconnect_locally( $has_valid_blog_id );
14098
14099 if (
14100 $change_owner &&
14101 // If successful ownership change.
14102 $fs->get_user()->id != $install->user_id
14103 ) {
14104 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
14105 }
14106 }
14107 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
14108 $result = $fs->activate_license_on_site( $user, $license_key );
14109 }
14110 }
14111
14112 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
14113 if ( FS_Api::is_blocked( $result ) ) {
14114 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
14115 }
14116
14117 $error = FS_Api::is_api_error_object( $result ) ?
14118 $result->error->message :
14119 var_export( $result, true );
14120 } else {
14121 $fs->network_upgrade_mode_completed();
14122
14123 $fs->_user = $user;
14124
14125 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
14126 $fs->_site = $fs->get_network_install();
14127 }
14128
14129 $fs->_sync_license( true, $has_valid_blog_id );
14130
14131 $this->maybe_sync_install_user();
14132
14133 $next_page = $fs->is_addon() ?
14134 $fs->get_parent_instance()->get_account_url() :
14135 $fs->get_after_activation_url( 'after_connect_url' );
14136 }
14137 } else {
14138 $next_page = $fs->opt_in(
14139 false,
14140 false,
14141 false,
14142 $license_key,
14143 false,
14144 false,
14145 false,
14146 $is_marketing_allowed,
14147 $sites
14148 );
14149
14150 if ( isset( $next_page->error ) ) {
14151 $error = $next_page->error;
14152 } else {
14153 if ( $is_network_activation_or_migration ) {
14154 /**
14155 * Get the list of sites that were just opted-in (and license activated).
14156 * This is an optimization for the next part below saving some DB queries.
14157 */
14158 $connected_sites = array();
14159 foreach ( $sites as $site ) {
14160 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
14161 $connected_sites[ $site['blog_id'] ] = true;
14162 }
14163 }
14164
14165 $all_sites = self::get_sites();
14166 $pending_blog_ids = array();
14167
14168 /**
14169 * 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.
14170 *
14171 * @author Vova Feldman (@svovaf)
14172 */
14173 foreach ( $all_sites as $site ) {
14174 $blog_id = self::get_site_blog_id( $site );
14175
14176 if ( isset( $connected_sites[ $blog_id ] ) ) {
14177 // Site was just connected.
14178 continue;
14179 }
14180
14181 if ( $fs->is_installed_on_site( $blog_id ) ) {
14182 // Site was already connected before.
14183 continue;
14184 }
14185
14186 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
14187 // Site's connection was delegated.
14188 continue;
14189 }
14190
14191 if ( $fs->is_anonymous_site( $blog_id ) ) {
14192 // Site connection was already skipped.
14193 continue;
14194 }
14195
14196 $pending_blog_ids[] = $blog_id;
14197 }
14198
14199 if ( ! empty( $pending_blog_ids ) ) {
14200 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14201 $fs->skip_connection( $pending_blog_ids );
14202 } else {
14203 $fs->delegate_connection( $pending_blog_ids );
14204 }
14205 }
14206 }
14207 }
14208 }
14209
14210 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14211 $fs->_storage->require_license_activation = false;
14212 }
14213
14214 $result = array(
14215 'success' => ( false === $error )
14216 );
14217
14218 if ( false !== $error ) {
14219 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14220 } else {
14221 if ( $fs->is_addon() || $fs->has_addons() ) {
14222 /**
14223 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14224 * an updated valid user licenses collection will be fetched from the server which is used to also
14225 * update the account add-ons (add-ons the user has licenses for).
14226 *
14227 * @author Leo Fajardo (@leorw)
14228 * @since 2.2.4
14229 */
14230 $fs->purge_valid_user_licenses_cache();
14231 }
14232
14233 $result['next_page'] = $next_page;
14234 }
14235
14236 return $result;
14237 }
14238
14239 /**
14240 * @author Leo Fajardo (@leorw)
14241 * @since 2.3.2
14242 *
14243 * @return array {
14244 * @key string Product slug.
14245 * @value array {
14246 * @property FS_Site $site
14247 * @property FS_Plugin_License $license
14248 * }
14249 * }
14250 */
14251 private function get_parent_and_addons_installs_info() {
14252 $fs = $this->is_addon() ?
14253 $this->get_parent_instance() :
14254 $this;
14255
14256 $installed_addons_ids = array();
14257
14258 $installed_addons_instances = $fs->get_installed_addons();
14259 foreach ( $installed_addons_instances as $instance ) {
14260 $installed_addons_ids[] = $instance->get_id();
14261 }
14262
14263 $addons_ids = array_unique( array_merge(
14264 $installed_addons_ids,
14265 $fs->get_updated_account_addons()
14266 ) );
14267
14268 // Add parent product info.
14269 $installs_info_by_slug_map = array(
14270 $fs->get_slug() => array(
14271 'install' => $fs->get_site(),
14272 'license' => $fs->_get_license()
14273 )
14274 );
14275
14276 foreach ( $addons_ids as $addon_id ) {
14277 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14278
14279 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14280
14281 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14282 // Add-on is not associated with an install entity.
14283 continue;
14284 }
14285
14286 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14287 'install' => $addon_info['site'],
14288 'license' => isset( $addon_info['license'] ) ?
14289 $addon_info['license'] :
14290 null
14291 );
14292 }
14293
14294 return $installs_info_by_slug_map;
14295 }
14296
14297 /**
14298 * @author Leo Fajardo (@leorw)
14299 * @since 1.2.3.1
14300 */
14301 function _network_activate_ajax_action() {
14302 $this->_logger->entrance();
14303
14304 $this->check_ajax_referer( 'network_activate' );
14305
14306 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14307 $fs = ( $plugin_id == $this->_module_id ) ?
14308 $this :
14309 $this->get_addon_instance( $plugin_id );
14310
14311 $error = false;
14312
14313 $sites = fs_request_get( 'sites', array(), 'post' );
14314 if ( is_array( $sites ) && ! empty( $sites ) ) {
14315 $sites_by_action = array(
14316 'allow' => array(),
14317 'delegate' => array(),
14318 'skip' => array()
14319 );
14320
14321 foreach ( $sites as $site ) {
14322 $sites_by_action[ $site['action'] ][] = $site;
14323 }
14324
14325 $total_sites = count( $sites );
14326 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14327
14328 $next_page = '';
14329
14330 $has_any_install = fs_request_get_bool( 'has_any_install' );
14331
14332 if ( $total_sites === $total_sites_to_delegate &&
14333 ! $this->is_network_upgrade_mode() &&
14334 ! $has_any_install
14335 ) {
14336 $this->delegate_connection();
14337 } else {
14338 if ( ! empty( $sites_by_action['delegate'] ) ) {
14339 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14340 }
14341
14342 if ( ! empty( $sites_by_action['skip'] ) ) {
14343 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14344 }
14345
14346 if ( empty( $sites_by_action['allow'] ) ) {
14347 if ( $has_any_install ) {
14348 $first_install = $fs->find_first_install();
14349
14350 if ( ! is_null( $first_install ) ) {
14351 $fs->_site = $first_install['install'];
14352 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14353
14354 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14355 $fs->_storage->network_user_id = $fs->_user->id;
14356 }
14357 }
14358 } else {
14359 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14360 $next_page = $fs->opt_in(
14361 false,
14362 false,
14363 false,
14364 false,
14365 false,
14366 false,
14367 false,
14368 fs_request_get_bool( 'is_marketing_allowed', null ),
14369 $sites_by_action['allow']
14370 );
14371 } else {
14372 $next_page = $fs->install_with_user(
14373 $this->get_network_user(),
14374 false,
14375 false,
14376 false,
14377 true,
14378 $sites_by_action['allow']
14379 );
14380 }
14381
14382 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14383 $error = $next_page->error;
14384 }
14385 }
14386 }
14387
14388 if ( empty( $next_page ) ) {
14389 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14390 }
14391 } else {
14392 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14393 }
14394
14395 $result = array(
14396 'success' => ( false === $error )
14397 );
14398
14399 if ( false !== $error ) {
14400 $result['error'] = $error;
14401 } else {
14402 $result['next_page'] = $next_page;
14403 }
14404
14405 echo json_encode( $result );
14406
14407 exit;
14408 }
14409
14410 /**
14411 * Billing update AJAX callback.
14412 *
14413 * @author Vova Feldman (@svovaf)
14414 * @since 1.2.1.5
14415 */
14416 function _update_billing_ajax_action() {
14417 $this->_logger->entrance();
14418
14419 $this->check_ajax_referer( 'update_billing' );
14420
14421 if ( ! $this->is_user_admin() ) {
14422 // Only for admins.
14423 self::shoot_ajax_failure();
14424 }
14425
14426 $billing = fs_request_get( 'billing' );
14427
14428 $api = $this->get_api_user_scope();
14429 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14430 'plugin_id' => $this->get_parent_id(),
14431 ) ) );
14432
14433 if ( ! $this->is_api_result_entity( $result ) ) {
14434 self::shoot_ajax_failure();
14435 }
14436
14437 // Purge cached billing.
14438 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14439
14440 self::shoot_ajax_success();
14441 }
14442
14443 /**
14444 * Trial start for anonymous users (AJAX callback).
14445 *
14446 * @author Vova Feldman (@svovaf)
14447 * @since 1.2.1.5
14448 */
14449 function _start_trial_ajax_action() {
14450 $this->_logger->entrance();
14451
14452 $this->check_ajax_referer( 'start_trial' );
14453
14454 if ( ! $this->is_user_admin() ) {
14455 // Only for admins.
14456 self::shoot_ajax_failure();
14457 }
14458
14459 $trial_data = fs_request_get( 'trial' );
14460
14461 $next_page = $this->opt_in(
14462 false,
14463 false,
14464 false,
14465 false,
14466 false,
14467 $trial_data['plan_id']
14468 );
14469
14470 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14471 self::shoot_ajax_failure(
14472 isset( $next_page->error ) ?
14473 $next_page->error->message :
14474 var_export( $next_page, true )
14475 );
14476 }
14477
14478 $this->shoot_ajax_success( array(
14479 'next_page' => $next_page,
14480 ) );
14481 }
14482
14483 /**
14484 * @author Leo Fajardo (@leorw)
14485 * @since 1.2.0
14486 */
14487 function _resend_license_key_ajax_action() {
14488 $this->_logger->entrance();
14489
14490 $this->check_ajax_referer( 'resend_license_key' );
14491
14492 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14493
14494 if ( empty( $email_address ) ) {
14495 exit;
14496 }
14497
14498 $error = false;
14499
14500 $api = $this->get_api_plugin_scope();
14501 $result = $api->call( '/licenses/resend.json', 'post',
14502 array(
14503 'email' => $email_address,
14504 'url' => home_url(),
14505 )
14506 );
14507
14508 if ( is_object( $result ) && isset( $result->error ) ) {
14509 $error = $result->error;
14510
14511 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14512 $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' );
14513 } else if ( 'no_license' === $error->code ) {
14514 $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' );
14515 } else {
14516 $error = $error->message;
14517 }
14518 }
14519
14520 $licenses = array(
14521 'success' => ( false === $error )
14522 );
14523
14524 if ( false !== $error ) {
14525 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14526 }
14527
14528 echo json_encode( $licenses );
14529
14530 exit;
14531 }
14532
14533 /**
14534 * @author Vova Feldman (@svovaf)
14535 * @since 1.2.1.8
14536 *
14537 * @var string
14538 */
14539 private static $_pagenow;
14540
14541 /**
14542 * Get current page or the referer if executing a WP AJAX request.
14543 *
14544 * @author Vova Feldman (@svovaf)
14545 * @since 1.2.1.8
14546 *
14547 * @return string
14548 */
14549 static function get_current_page() {
14550 if ( ! isset( self::$_pagenow ) ) {
14551 global $pagenow;
14552 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14553 /**
14554 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14555 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14556 *
14557 * @author Leo Fajardo (@leorw)
14558 * @since 2.2.3
14559 */
14560 if ( is_network_admin() ) {
14561 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14562 } else if ( is_user_admin() ) {
14563 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14564 } else {
14565 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14566 }
14567
14568 $pagenow = $self_matches[1];
14569 $pagenow = trim( $pagenow, '/' );
14570 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14571 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14572 $pagenow = 'index.php';
14573 } else {
14574 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14575 $pagenow = strtolower( $self_matches[1] );
14576 if ( '.php' !== substr($pagenow, -4, 4) )
14577 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14578 }
14579 }
14580
14581 self::$_pagenow = $pagenow;
14582
14583 if ( self::is_ajax() &&
14584 'admin-ajax.php' === $pagenow
14585 ) {
14586 $referer = fs_get_raw_referer();
14587
14588 if ( is_string( $referer ) ) {
14589 $parts = explode( '?', $referer );
14590
14591 self::$_pagenow = basename( $parts[0] );
14592 }
14593 }
14594 }
14595
14596 return self::$_pagenow;
14597 }
14598
14599 /**
14600 * Helper method to check if user in the plugins page.
14601 *
14602 * @author Vova Feldman (@svovaf)
14603 * @since 1.2.1.5
14604 *
14605 * @return bool
14606 */
14607 static function is_plugins_page() {
14608 return ( 'plugins.php' === self::get_current_page() );
14609 }
14610
14611 /**
14612 * @author Leo Fajardo (@leorw)
14613 * @since 2.2.3
14614 *
14615 * @return bool
14616 */
14617 static function is_plugin_install_page() {
14618 return ( 'plugin-install.php' === self::get_current_page() );
14619 }
14620
14621 /**
14622 * @author Leo Fajardo (@leorw)
14623 * @since 2.0.2
14624 *
14625 * @return bool
14626 */
14627 static function is_updates_page() {
14628 return ( 'update-core.php' === self::get_current_page() );
14629 }
14630
14631 /**
14632 * Helper method to check if user in the themes page.
14633 *
14634 * @author Vova Feldman (@svovaf)
14635 * @since 1.2.2.6
14636 *
14637 * @return bool
14638 */
14639 static function is_themes_page() {
14640 return ( 'themes.php' === self::get_current_page() );
14641 }
14642
14643 #----------------------------------------------------------------------------------
14644 #region Affiliation
14645 #----------------------------------------------------------------------------------
14646
14647 /**
14648 * @author Leo Fajardo (@leorw)
14649 * @since 1.2.3
14650 *
14651 * @return bool
14652 */
14653 function has_affiliate_program() {
14654 if ( ! is_object( $this->_plugin ) ) {
14655 return false;
14656 }
14657
14658 return $this->_plugin->has_affiliate_program();
14659 }
14660
14661 /**
14662 * Get Plugin ID under which we will track affiliate application.
14663 *
14664 * This could either be the Bundle ID or the main plugin ID.
14665 *
14666 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14667 */
14668 private function get_plugin_id_for_affiliate_terms() {
14669 return $this->has_bundle_context() ?
14670 $this->get_bundle_id() :
14671 $this->_plugin->id;
14672 }
14673
14674 /**
14675 * @author Leo Fajardo (@leorw)
14676 * @since 1.2.4
14677 */
14678 private function fetch_affiliate_terms() {
14679 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14680 /**
14681 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14682 */
14683 $plugins_api = $this->has_bundle_context() ?
14684 $this->get_api_bundle_scope() :
14685 $this->get_api_plugin_scope();
14686
14687 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14688
14689 /**
14690 * 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.
14691 */
14692 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14693 return;
14694 }
14695
14696 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14697 }
14698 }
14699
14700 /**
14701 * @author Leo Fajardo (@leorw)
14702 * @since 1.2.4
14703 */
14704 private function fetch_affiliate_and_custom_terms() {
14705 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14706 $application_data = $this->_storage->affiliate_application_data;
14707 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14708
14709 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14710
14711 $users_api = $this->get_api_user_scope();
14712 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14713 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14714 if ( ! empty( $result->affiliates ) ) {
14715 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14716
14717 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14718 $application_data['status'] = $affiliate->status;
14719 $this->_storage->affiliate_application_data = $application_data;
14720 }
14721
14722 if ( $affiliate->is_using_custom_terms ) {
14723 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14724 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14725 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14726 }
14727 }
14728
14729 $this->affiliate = $affiliate;
14730 }
14731 }
14732 }
14733 }
14734
14735 /**
14736 * @author Leo Fajardo (@leorw)
14737 * @since 1.2.3
14738 */
14739 private function fetch_affiliate_and_terms() {
14740 $this->_logger->entrance();
14741
14742 $this->fetch_affiliate_terms();
14743 $this->fetch_affiliate_and_custom_terms();
14744 }
14745
14746 /**
14747 * @author Leo Fajardo (@leorw)
14748 * @since 1.2.3
14749 *
14750 * @return FS_Affiliate
14751 */
14752 function get_affiliate() {
14753 return $this->affiliate;
14754 }
14755
14756
14757 /**
14758 * @author Leo Fajardo (@leorw)
14759 * @since 1.2.3
14760 *
14761 * @return FS_AffiliateTerms
14762 */
14763 function get_affiliate_terms() {
14764 return is_object( $this->custom_affiliate_terms ) ?
14765 $this->custom_affiliate_terms :
14766 $this->plugin_affiliate_terms;
14767 }
14768
14769 /**
14770 * @author Leo Fajardo (@leorw)
14771 * @since 1.2.3
14772 */
14773 function _submit_affiliate_application() {
14774 $this->_logger->entrance();
14775
14776 $this->check_ajax_referer( 'submit_affiliate_application' );
14777
14778 if ( ! $this->is_user_admin() ) {
14779 // Only for admins.
14780 self::shoot_ajax_failure();
14781 }
14782
14783 $affiliate = fs_request_get( 'affiliate' );
14784
14785 if ( empty( $affiliate['promotion_methods'] ) ) {
14786 unset( $affiliate['promotion_methods'] );
14787 }
14788
14789 if ( ! empty( $affiliate['additional_domains'] ) ) {
14790 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14791 }
14792
14793 if ( ! $this->is_registered() ) {
14794 // Opt in but don't track usage.
14795 $next_page = $this->opt_in(
14796 false,
14797 false,
14798 false,
14799 false,
14800 false,
14801 false,
14802 true
14803 );
14804
14805 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14806 self::shoot_ajax_failure(
14807 isset( $next_page->error ) ?
14808 $next_page->error->message :
14809 var_export( $next_page, true )
14810 );
14811 } else if ( $this->is_pending_activation() ) {
14812 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' ) );
14813 }
14814 }
14815
14816 $this->fetch_affiliate_terms();
14817
14818 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14819
14820 $api = $this->get_api_user_scope();
14821 $result = $api->call(
14822 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14823 'post',
14824 $affiliate
14825 );
14826
14827 if ( $this->is_api_error( $result ) ) {
14828 self::shoot_ajax_failure(
14829 isset( $result->error ) ?
14830 $result->error->message :
14831 var_export( $result, true )
14832 );
14833 } else {
14834 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14835 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14836 }
14837
14838 $affiliate_application_data = array(
14839 'status' => 'pending',
14840 'stats_description' => $affiliate['stats_description'],
14841 'promotion_method_description' => $affiliate['promotion_method_description'],
14842 );
14843
14844 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14845 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14846 }
14847
14848 if ( ! empty( $affiliate['domain'] ) ) {
14849 $affiliate_application_data['domain'] = $affiliate['domain'];
14850 }
14851
14852 if ( ! empty( $affiliate['additional_domains'] ) ) {
14853 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14854 }
14855
14856 $this->_storage->affiliate_application_data = $affiliate_application_data;
14857 }
14858
14859 // Purge cached affiliate.
14860 $api->purge_cache( 'affiliate.json' );
14861
14862 self::shoot_ajax_success( $result );
14863 }
14864
14865 /**
14866 * @author Leo Fajardo (@leorw)
14867 * @since 1.2.3
14868 *
14869 * @return array|null
14870 */
14871 function get_affiliate_application_data() {
14872 if ( empty( $this->_storage->affiliate_application_data ) ) {
14873 return null;
14874 }
14875
14876 return $this->_storage->affiliate_application_data;
14877 }
14878
14879 #endregion Affiliation ------------------------------------------------------------
14880
14881 #----------------------------------------------------------------------------------
14882 #region URL Generators
14883 #----------------------------------------------------------------------------------
14884
14885 /**
14886 * Alias to pricing_url().
14887 *
14888 * @author Vova Feldman (@svovaf)
14889 * @since 1.0.2
14890 *
14891 * @uses pricing_url()
14892 *
14893 * @param string $period Billing cycle
14894 * @param bool $is_trial
14895 *
14896 * @return string
14897 */
14898 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14899 return $this->pricing_url( $period, $is_trial );
14900 }
14901
14902 /**
14903 * @author Vova Feldman (@svovaf)
14904 * @since 1.0.9
14905 *
14906 * @uses get_upgrade_url()
14907 *
14908 * @return string
14909 */
14910 function get_trial_url() {
14911 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14912 }
14913
14914 /**
14915 * @author Leo Fajardo (@leorw)
14916 * @since 2.1.4
14917 *
14918 * @param string $new_version
14919 *
14920 * @return string
14921 */
14922 function version_upgrade_checkout_link( $new_version ) {
14923 if ( ! is_object( $this->_license ) ) {
14924 $url = $this->pricing_url();
14925
14926 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14927 } else {
14928 $subscription = $this->_get_subscription( $this->_license->id );
14929
14930 $url = $this->checkout_url(
14931 is_object( $subscription ) ?
14932 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14933 WP_FS__PERIOD_LIFETIME,
14934 false,
14935 array( 'licenses' => $this->_license->quota )
14936 );
14937
14938 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14939 }
14940
14941 return sprintf(
14942 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14943 sprintf(
14944 '<a href="%s">%s</a>',
14945 $this->apply_filters( 'update_notice_checkout_url', $url ),
14946 $purchase_license_text
14947 ),
14948 $new_version
14949 );
14950 }
14951
14952 /**
14953 * Plugin's pricing URL.
14954 *
14955 * @author Vova Feldman (@svovaf)
14956 * @since 1.0.4
14957 *
14958 * @param string $billing_cycle Billing cycle
14959 *
14960 * @param bool $is_trial
14961 *
14962 * @return string
14963 */
14964 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14965 $this->_logger->entrance();
14966
14967 $params = array(
14968 'billing_cycle' => $billing_cycle
14969 );
14970
14971 if ( $is_trial ) {
14972 $params['trial'] = 'true';
14973 }
14974
14975 $url = $this->is_addon() ?
14976 $this->_parent->addon_url( $this->_slug ) :
14977 $this->_get_admin_page_url( 'pricing', $params );
14978
14979 return $this->apply_filters( 'pricing_url', $url );
14980 }
14981
14982 /**
14983 * Checkout page URL.
14984 *
14985 * @author Vova Feldman (@svovaf)
14986 * @since 1.0.6
14987 *
14988 * @param string $billing_cycle Billing cycle
14989 * @param bool $is_trial
14990 * @param array $extra (optional) Extra parameters, override other query params.
14991 * @param bool|null $network
14992 *
14993 * @return string
14994 */
14995 function checkout_url(
14996 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14997 $is_trial = false,
14998 $extra = array(),
14999 $network = null
15000 ) {
15001 $this->_logger->entrance();
15002
15003 $params = array(
15004 'checkout' => 'true',
15005 'billing_cycle' => $billing_cycle,
15006 );
15007
15008 if ( $is_trial ) {
15009 $params['trial'] = 'true';
15010 }
15011
15012 /**
15013 * Params in extra override other params.
15014 */
15015 $params = array_merge( $params, $extra );
15016
15017 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
15018 }
15019
15020 /**
15021 * Add-on checkout URL.
15022 *
15023 * @author Vova Feldman (@svovaf)
15024 * @since 1.1.7
15025 *
15026 * @param number $addon_id
15027 * @param number $pricing_id
15028 * @param string $billing_cycle
15029 * @param bool $is_trial
15030 * @param bool|null $network
15031 *
15032 * @return string
15033 */
15034 function addon_checkout_url(
15035 $addon_id,
15036 $pricing_id,
15037 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15038 $is_trial = false,
15039 $network = null
15040 ) {
15041 return $this->checkout_url( $billing_cycle, $is_trial, array(
15042 'plugin_id' => $addon_id,
15043 'pricing_id' => $pricing_id,
15044 ), $network );
15045 }
15046
15047 #endregion
15048
15049 #endregion ------------------------------------------------------------------
15050
15051 /**
15052 * Check if plugin has any add-ons.
15053 *
15054 * @author Vova Feldman (@svovaf)
15055 * @since 1.0.5
15056 *
15057 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
15058 *
15059 * @return bool
15060 */
15061 function has_addons() {
15062 $this->_logger->entrance();
15063
15064 return $this->_has_addons;
15065 }
15066
15067 /**
15068 * Check if plugin can work in anonymous mode.
15069 *
15070 * @author Vova Feldman (@svovaf)
15071 * @since 1.0.9
15072 *
15073 * @return bool
15074 *
15075 * @deprecated Please use is_enable_anonymous() instead.
15076 */
15077 function enable_anonymous() {
15078 return $this->_enable_anonymous;
15079 }
15080
15081 /**
15082 * Check if plugin can work in anonymous mode.
15083 *
15084 * @author Vova Feldman (@svovaf)
15085 * @since 1.1.9
15086 *
15087 * @return bool
15088 */
15089 function is_enable_anonymous() {
15090 return $this->_enable_anonymous;
15091 }
15092
15093 /**
15094 * Check if plugin is premium only (no free plans).
15095 *
15096 * @author Vova Feldman (@svovaf)
15097 * @since 1.1.9
15098 *
15099 * @return bool
15100 */
15101 function is_only_premium() {
15102 return $this->_is_premium_only;
15103 }
15104
15105 /**
15106 * Checks if the plugin's type is "plugin". The other type is "theme".
15107 *
15108 * @author Leo Fajardo (@leorw)
15109 * @since 1.2.2
15110 *
15111 * @return bool
15112 */
15113 function is_plugin() {
15114 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
15115 }
15116
15117 /**
15118 * @author Leo Fajardo (@leorw)
15119 * @since 1.2.2
15120 *
15121 * @return string
15122 */
15123 function get_module_type() {
15124 if ( ! isset( $this->_module_type ) ) {
15125 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
15126 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
15127 }
15128
15129 return $this->_module_type;
15130 }
15131
15132 /**
15133 * @author Leo Fajardo (@leorw)
15134 * @since 1.2.2
15135 *
15136 * @return string
15137 */
15138 function get_plugin_main_file_path() {
15139 return $this->_plugin_main_file_path;
15140 }
15141
15142 /**
15143 * Check if module has a premium code version.
15144 *
15145 * Serviceware module might be freemium without any
15146 * premium code version, where the paid features
15147 * are all part of the service.
15148 *
15149 * @author Vova Feldman (@svovaf)
15150 * @since 1.2.1.6
15151 *
15152 * @return bool
15153 */
15154 function has_premium_version() {
15155 return $this->_has_premium_version;
15156 }
15157
15158 /**
15159 * Check if feature supported with current site's plan.
15160 *
15161 * @author Vova Feldman (@svovaf)
15162 * @since 1.0.1
15163 *
15164 * @todo IMPLEMENT
15165 *
15166 * @param number $feature_id
15167 *
15168 * @throws Exception
15169 */
15170 function is_feature_supported( $feature_id ) {
15171 throw new Exception( 'not implemented' );
15172 }
15173
15174 /**
15175 * @author Vova Feldman (@svovaf)
15176 * @since 1.0.1
15177 *
15178 * @return bool Is running in SSL/HTTPS
15179 */
15180 function is_ssl() {
15181 return WP_FS__IS_HTTPS;
15182 }
15183
15184 /**
15185 * @author Vova Feldman (@svovaf)
15186 * @since 1.0.9
15187 *
15188 * @return bool Is running in AJAX call.
15189 *
15190 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15191 */
15192 static function is_ajax() {
15193 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15194 }
15195
15196 /**
15197 * Check if it's an AJAX call targeted for the current module.
15198 *
15199 * @author Vova Feldman (@svovaf)
15200 * @since 1.2.0
15201 *
15202 * @param array|string $actions Collection of AJAX actions.
15203 *
15204 * @return bool
15205 */
15206 function is_ajax_action( $actions ) {
15207 // Verify it's an ajax call.
15208 if ( ! self::is_ajax() ) {
15209 return false;
15210 }
15211
15212 // Verify the call is relevant for the plugin.
15213 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15214 return false;
15215 }
15216
15217 // Verify it's one of the specified actions.
15218 if ( is_string( $actions ) ) {
15219 $actions = explode( ',', $actions );
15220 }
15221
15222 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15223 $ajax_action = fs_request_get( 'action' );
15224
15225 foreach ( $actions as $action ) {
15226 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15227 return true;
15228 }
15229 }
15230 }
15231
15232 return false;
15233 }
15234
15235 /**
15236 * Check if it's an AJAX call targeted for current request.
15237 *
15238 * @author Vova Feldman (@svovaf)
15239 * @since 1.2.0
15240 *
15241 * @param array|string $actions Collection of AJAX actions.
15242 * @param number|null $module_id
15243 *
15244 * @return bool
15245 */
15246 static function is_ajax_action_static( $actions, $module_id = null ) {
15247 // Verify it's an ajax call.
15248 if ( ! self::is_ajax() ) {
15249 return false;
15250 }
15251
15252
15253 if ( ! empty( $module_id ) ) {
15254 // Verify the call is relevant for the plugin.
15255 if ( $module_id != fs_request_get( 'module_id' ) ) {
15256 return false;
15257 }
15258 }
15259
15260 // Verify it's one of the specified actions.
15261 if ( is_string( $actions ) ) {
15262 $actions = explode( ',', $actions );
15263 }
15264
15265 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15266 $ajax_action = fs_request_get( 'action' );
15267
15268 foreach ( $actions as $action ) {
15269 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15270 return true;
15271 }
15272 }
15273 }
15274
15275 return false;
15276 }
15277
15278 /**
15279 * @author Vova Feldman (@svovaf)
15280 * @since 1.1.7
15281 *
15282 * @return bool
15283 */
15284 static function is_cron() {
15285 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15286 }
15287
15288 /**
15289 * @author Leo Fajardo (@leorw)
15290 * @since 2.5.0
15291 *
15292 * @return bool
15293 */
15294 static function is_admin_post() {
15295 return ( 'admin-post.php' === self::get_current_page() );
15296 }
15297
15298 /**
15299 * Check if a real user is visiting the admin dashboard.
15300 *
15301 * @author Vova Feldman (@svovaf)
15302 * @since 1.1.7
15303 *
15304 * @return bool
15305 */
15306 function is_user_in_admin() {
15307 return (
15308 is_admin() &&
15309 ! self::is_ajax() &&
15310 ! self::is_cron() &&
15311 ! self::is_admin_post()
15312 );
15313 }
15314
15315 /**
15316 * Check if a real user is in the customizer view.
15317 *
15318 * @author Vova Feldman (@svovaf)
15319 * @since 1.2.2.7
15320 *
15321 * @return bool
15322 */
15323 static function is_customizer() {
15324 return is_customize_preview();
15325 }
15326
15327 /**
15328 * Check if running in HTTPS and if site's plan matching the specified plan.
15329 *
15330 * @param string $plan
15331 * @param bool $exact
15332 *
15333 * @return bool
15334 */
15335 function is_ssl_and_plan( $plan, $exact = false ) {
15336 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15337 }
15338
15339 /**
15340 * Construct plugin's settings page URL.
15341 *
15342 * @author Vova Feldman (@svovaf)
15343 * @since 1.0.4
15344 *
15345 * @param string $page
15346 * @param array $params
15347 * @param bool|null $network
15348 *
15349 * @return string
15350 */
15351 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15352 if ( is_null( $network ) ) {
15353 $network = (
15354 $this->_is_network_active &&
15355 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15356 );
15357 }
15358
15359 if ( 0 < count( $params ) ) {
15360 foreach ( $params as $k => $v ) {
15361 $params[ $k ] = urlencode( $v );
15362 }
15363 }
15364
15365 $page_param = $this->_menu->get_slug( $page );
15366
15367 if ( empty( $page ) &&
15368 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15369 $this->show_opt_in_on_themes_page()
15370 ) {
15371 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15372
15373 return add_query_arg(
15374 $params,
15375 $this->admin_url( 'themes.php', 'admin', $network )
15376 );
15377 }
15378
15379 if ( ! $this->has_settings_menu() ) {
15380 if ( ! empty( $page ) ) {
15381 // Module doesn't have a setting page, but since the request is for
15382 // a specific Freemius page, use the admin.php path.
15383 return add_query_arg( array_merge( $params, array(
15384 'page' => $page_param,
15385 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15386 } else {
15387 if ( $this->is_activation_mode() ) {
15388 /**
15389 * @author Vova Feldman
15390 * @since 1.2.1.6
15391 *
15392 * If plugin doesn't have a settings page, create one for the opt-in screen.
15393 */
15394 return add_query_arg( array_merge( $params, array(
15395 'page' => $this->_slug,
15396 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15397 } else {
15398 // Plugin without a settings page.
15399 return add_query_arg(
15400 $params,
15401 $this->admin_url( 'plugins.php', 'admin', $network )
15402 );
15403 }
15404 }
15405 }
15406
15407 // Module has a submenu settings page.
15408 if ( ! $this->_menu->is_top_level() ) {
15409 $parent_slug = $this->_menu->get_parent_slug();
15410 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15411 $parent_slug :
15412 'admin.php';
15413
15414 return add_query_arg( array_merge( $params, array(
15415 'page' => $page_param,
15416 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15417 }
15418
15419 // Module has a top level CPT settings page.
15420 if ( $this->_menu->is_cpt() ) {
15421 if ( empty( $page ) && $this->is_activation_mode() ) {
15422 return add_query_arg( array_merge( $params, array(
15423 'page' => $page_param
15424 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15425 } else {
15426 if ( ! empty( $page ) ) {
15427 $params['page'] = $page_param;
15428 }
15429
15430 return add_query_arg(
15431 $params,
15432 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15433 );
15434 }
15435 }
15436
15437 // Module has a custom top level settings page.
15438 return add_query_arg( array_merge( $params, array(
15439 'page' => $page_param,
15440 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15441 }
15442
15443 #--------------------------------------------------------------------------------
15444 #region Multisite
15445 #--------------------------------------------------------------------------------
15446
15447 /**
15448 * @author Leo Fajardo (@leorw)
15449 * @since 2.0.0
15450 *
15451 * @return bool
15452 */
15453 function is_network_active() {
15454 return $this->_is_network_active;
15455 }
15456
15457 /**
15458 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15459 *
15460 * @author Leo Fajardo (@leorw)
15461 * @since 2.0.0
15462 *
15463 * @param bool|int[] $all_or_blog_ids
15464 */
15465 private function delegate_connection( $all_or_blog_ids = true ) {
15466 $this->_logger->entrance();
15467
15468 $this->_admin_notices->remove_sticky( 'connect_account' );
15469
15470 if ( true === $all_or_blog_ids ) {
15471 // All sites delegation.
15472 $this->_storage->store( 'is_delegated_connection', true, true );
15473 } else {
15474 // Specified sites delegation.
15475 foreach ( $all_or_blog_ids as $blog_id ) {
15476 $this->delegate_site_connection( $blog_id );
15477 }
15478 }
15479
15480 $this->network_upgrade_mode_completed();
15481 }
15482
15483 /**
15484 * Delegate specific network site conncetion to the site admin.
15485 *
15486 * @author Vova Feldman (@svovaf)
15487 * @since 2.0.0
15488 *
15489 * @param int $blog_id
15490 */
15491 private function delegate_site_connection( $blog_id ) {
15492 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15493 }
15494
15495 /**
15496 * Check if super-admin delegated the connection of ALL sites to the site admins.
15497 *
15498 * @author Vova Feldman (@svovaf)
15499 * @since 2.0.0
15500 *
15501 * @return bool
15502 */
15503 function is_network_delegated_connection() {
15504 if ( ! $this->_is_network_active ) {
15505 return false;
15506 }
15507
15508 return $this->_storage->get( 'is_delegated_connection', false, true );
15509 }
15510
15511 /**
15512 * @author Leo Fajardo (@leorw)
15513 * @since 2.0.0
15514 *
15515 * @param int $blog_id
15516 *
15517 * @return bool
15518 */
15519 function is_site_delegated_connection( $blog_id = 0 ) {
15520 if ( ! $this->_is_network_active ) {
15521 return false;
15522 }
15523
15524 if ( 0 == $blog_id ) {
15525 $blog_id = get_current_blog_id();
15526 }
15527
15528 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15529 }
15530
15531 /**
15532 * Check if delegated the connection. When running within the network admin,
15533 * and haven't specified the blog ID, checks if network level delegated. If running
15534 * within a site admin or specified a blog ID, check if delegated the connection for
15535 * the current context site.
15536 *
15537 * If executed outside the the admin, check if delegated the connection
15538 * for the current context site OR the whole network.
15539 *
15540 * @author Vova Feldman (@svovaf)
15541 * @since 2.0.0
15542 *
15543 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15544 *
15545 * @return bool
15546 */
15547 function is_delegated_connection( $blog_id = 0 ) {
15548 if ( ! $this->_is_network_active ) {
15549 return false;
15550 }
15551
15552 if ( fs_is_network_admin() && 0 == $blog_id ) {
15553 return $this->is_network_delegated_connection();
15554 }
15555
15556 return (
15557 $this->is_network_delegated_connection() ||
15558 $this->is_site_delegated_connection( $blog_id )
15559 );
15560 }
15561
15562 /**
15563 * Check if the current module is active for the site.
15564 *
15565 * @author Vova Feldman (@svovaf)
15566 * @since 2.0.0
15567 *
15568 * @param int $blog_id
15569 *
15570 * @return bool
15571 */
15572 function is_active_for_site( $blog_id ) {
15573 if ( ! is_multisite() ) {
15574 // Not a multisite and this code is executed, means that the plugin is active.
15575 return true;
15576 }
15577
15578 if ( $this->is_theme() ) {
15579 // All themes are site level activated.
15580 return true;
15581 }
15582
15583 if ( $this->_is_network_active ) {
15584 // Plugin was network activated so it's active.
15585 return true;
15586 }
15587
15588 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15589 }
15590
15591 /**
15592 * @todo Implement pagination when accessing the subsites collection.
15593 *
15594 * @author Leo Fajardo (@leorw)
15595 * @since 2.0.0
15596 *
15597 * @param int $limit Default to 1,000
15598 * @param int $offset Default to 0
15599 *
15600 * @return array Active & public sites collection.
15601 */
15602 static function get_sites( $limit = 1000, $offset = 0 ) {
15603 if ( ! is_multisite() ) {
15604 return array();
15605 }
15606
15607 /**
15608 * For consistency with get_blog_list() which only return active public sites.
15609 *
15610 * @author Vova Feldman (@svovaf)
15611 */
15612 $args = array(
15613 /**
15614 * Commented out in order to handle the migration of site options whether the site is public or not.
15615 *
15616 * @author Leo Fajardo (@leorw)
15617 * @since 2.2.1
15618 */
15619 // 'public' => 1,
15620 'archived' => 0,
15621 'mature' => 0,
15622 'spam' => 0,
15623 'deleted' => 0,
15624 'number' => $limit,
15625 'offset' => $offset,
15626 );
15627
15628 return get_sites( $args );
15629 }
15630
15631 /**
15632 * Checks if a given blog is active.
15633 *
15634 * @author Vova Feldman (@svovaf)
15635 * @since 2.0.0
15636 *
15637 * @param $blog_id
15638 *
15639 * @return bool
15640 */
15641 private static function is_site_active( $blog_id ) {
15642 global $wpdb;
15643
15644 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15645
15646 if ( ! is_object( $blog_info ) ) {
15647 return false;
15648 }
15649
15650 return (
15651 true == $blog_info->public &&
15652 false == $blog_info->archived &&
15653 false == $blog_info->mature &&
15654 false == $blog_info->spam &&
15655 false == $blog_info->deleted
15656 );
15657 }
15658
15659 /**
15660 * Get a mapping between the site addresses to their blog IDs.
15661 *
15662 * @author Vova Feldman (@svovaf)
15663 * @since 2.0.0
15664 *
15665 * @return array {
15666 * @key string Site address without protocol with a trailing slash.
15667 * @value int Site's blog ID.
15668 * }
15669 */
15670 private function get_address_to_blog_map() {
15671 $sites = self::get_sites();
15672
15673 // Map site addresses to their blog IDs.
15674 $address_to_blog_map = array();
15675 foreach ( $sites as $site ) {
15676 $blog_id = self::get_site_blog_id( $site );
15677 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15678 $address_to_blog_map[ $address ] = $blog_id;
15679 }
15680
15681 return $address_to_blog_map;
15682 }
15683
15684 /**
15685 * Get a mapping between the site addresses to their blog IDs.
15686 *
15687 * @author Vova Feldman (@svovaf)
15688 * @since 2.0.0
15689 *
15690 * @return array {
15691 * @key int Site's blog ID.
15692 * @value FS_Site Associated install.
15693 * }
15694 */
15695 function get_blog_install_map() {
15696 $sites = self::get_sites();
15697
15698 // Map site blog ID to its install.
15699 $install_map = array();
15700
15701 foreach ( $sites as $site ) {
15702 $blog_id = self::get_site_blog_id( $site );
15703 $install = $this->get_install_by_blog_id( $blog_id );
15704
15705 if ( is_object( $install ) ) {
15706 $install_map[ $blog_id ] = $install;
15707 }
15708 }
15709
15710 return $install_map;
15711 }
15712
15713 /**
15714 * @author Vova Feldman (@svovaf)
15715 * @since 2.5.1
15716 *
15717 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15718 *
15719 * @return int[]
15720 */
15721 private function get_blog_ids( $is_delegated = null ) {
15722 $blog_ids = array();
15723
15724 $sites = self::get_sites();
15725 foreach ( $sites as $site ) {
15726 $blog_id = self::get_site_blog_id( $site );
15727
15728 if (
15729 is_null( $is_delegated ) ||
15730 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15731 ) {
15732 $blog_ids[] = $blog_id;
15733 }
15734 }
15735
15736 return $blog_ids;
15737 }
15738
15739 /**
15740 * @author Vova Feldman (@svovaf)
15741 * @since 2.5.1
15742 *
15743 * @return int[]
15744 */
15745 private function get_non_delegated_blog_ids() {
15746 return $this->get_blog_ids( false );
15747 }
15748
15749 /**
15750 * Gets a map of module IDs that the given user has opted-in to.
15751 *
15752 * @author Leo Fajardo (@leorw)
15753 * @since 2.1.0
15754 *
15755 * @param number $fs_user_id
15756 *
15757 * @return array {
15758 * @key number $plugin_id
15759 * @value bool Always true.
15760 * }
15761 */
15762 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15763 self::$_static_logger->entrance();
15764
15765 if ( ! is_multisite() ) {
15766 $installs = array_merge(
15767 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15768 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15769 );
15770 } else {
15771 $sites = self::get_sites();
15772
15773 $installs = array();
15774 foreach ( $sites as $site ) {
15775 $blog_id = self::get_site_blog_id( $site );
15776
15777 $installs = array_merge(
15778 $installs,
15779 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15780 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15781 );
15782 }
15783 }
15784
15785 $module_ids_map = array();
15786 foreach ( $installs as $install ) {
15787 if ( is_object( $install ) &&
15788 FS_Site::is_valid_id( $install->id ) &&
15789 FS_User::is_valid_id( $install->user_id ) &&
15790 ( $install->user_id == $fs_user_id )
15791 ) {
15792 $module_ids_map[ $install->plugin_id ] = true;
15793 }
15794 }
15795
15796 return $module_ids_map;
15797 }
15798
15799 /**
15800 * @author Leo Fajardo (@leorw)
15801 *
15802 * @return null|array {
15803 * 'install' => FS_Site Module's install,
15804 * 'blog_id' => string The associated blog ID.
15805 * }
15806 */
15807 function find_first_install() {
15808 $sites = self::get_sites();
15809
15810 foreach ( $sites as $site ) {
15811 $blog_id = self::get_site_blog_id( $site );
15812 $install = $this->get_install_by_blog_id( $blog_id );
15813
15814 if ( is_object( $install ) ) {
15815 return array(
15816 'install' => $install,
15817 'blog_id' => $blog_id
15818 );
15819 }
15820 }
15821
15822 return null;
15823 }
15824
15825 /**
15826 * Switches the Freemius site level context to a specified blog.
15827 *
15828 * @author Vova Feldman (@svovaf)
15829 * @since 2.0.0
15830 *
15831 * @param int $blog_id
15832 * @param FS_Site $install
15833 * @param bool $flush
15834 *
15835 * @return bool Since 2.3.1 returns if a switch was made.
15836 */
15837 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
15838 if ( ! is_numeric( $blog_id ) ) {
15839 return false;
15840 }
15841
15842 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15843 return false;
15844 }
15845
15846 switch_to_blog( $blog_id );
15847 $this->_context_is_network_or_blog_id = $blog_id;
15848
15849 self::$_accounts->set_site_blog_context( $blog_id );
15850 $this->_storage->set_site_blog_context( $blog_id );
15851 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15852
15853 $this->_site = is_object( $install ) ?
15854 $install :
15855 $this->get_install_by_blog_id( $blog_id );
15856
15857 $this->_user = false;
15858 $this->_licenses = false;
15859 $this->_license = null;
15860 $this->is_whitelabeled = null;
15861
15862 if ( is_object( $this->_site ) ) {
15863 // Try to fetch user from install.
15864 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15865
15866 if ( ! is_object( $this->_user ) &&
15867 FS_User::is_valid_id( $this->_storage->prev_user_id )
15868 ) {
15869 // Try to fetch previously saved user.
15870 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15871
15872 if ( ! is_object( $this->_user ) ) {
15873 // Fallback to network's user.
15874 $this->_user = $this->get_network_user();
15875 }
15876 }
15877
15878 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15879
15880 if ( ! empty( $all_plugin_licenses ) ) {
15881 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15882 $this->_license = null;
15883 } else {
15884 $license_found = false;
15885 foreach ( $all_plugin_licenses as $license ) {
15886 if ( $license->id == $this->_site->license_id ) {
15887 // License found.
15888 $this->_license = $license;
15889 $license_found = true;
15890 break;
15891 }
15892 }
15893
15894 if ( $license_found ) {
15895 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15896 }
15897 }
15898
15899 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15900 }
15901 }
15902
15903 unset( $this->_site_api );
15904 unset( $this->_user_api );
15905
15906 return true;
15907 }
15908
15909 /**
15910 * Restore the blog context to the blog that originally loaded the module.
15911 *
15912 * @author Vova Feldman (@svovaf)
15913 * @since 2.0.0
15914 */
15915 function restore_current_blog() {
15916 $this->switch_to_blog( $this->_blog_id );
15917 }
15918
15919 /**
15920 * @author Vova Feldman (@svovaf)
15921 * @since 2.0.0
15922 *
15923 * @param array|WP_Site $site
15924 *
15925 * @return int
15926 */
15927 static function get_site_blog_id( &$site ) {
15928 return ( $site instanceof WP_Site ) ?
15929 $site->blog_id :
15930 ( is_object( $site ) && isset( $site->userblog_id ) ?
15931 $site->userblog_id :
15932 $site['blog_id'] );
15933 }
15934
15935 /**
15936 * @author Vova Feldman (@svovaf)
15937 * @since 2.5.1
15938 *
15939 * @param WP_Site[]|array[] $sites
15940 *
15941 * @return int[]
15942 */
15943 static function get_sites_blog_ids( $sites ) {
15944 $blog_ids = array();
15945 foreach ( $sites as $site ) {
15946 $blog_ids[] = self::get_site_blog_id( $site );
15947 }
15948
15949 return $blog_ids;
15950 }
15951
15952 /**
15953 * @author Leo Fajardo (@leorw)
15954 * @since 2.0.0
15955 *
15956 * @param array|WP_Site|null $site
15957 * @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.
15958 *
15959 * @return array
15960 */
15961 function get_site_info( $site = null, $load_registration = false ) {
15962 $this->_logger->entrance();
15963
15964 $switched = false;
15965
15966 $registration_date = null;
15967
15968 if ( is_null( $site ) ) {
15969 $url = self::get_unfiltered_site_url();
15970 $name = get_bloginfo( 'name' );
15971 $blog_id = null;
15972 } else {
15973 $blog_id = self::get_site_blog_id( $site );
15974
15975 if ( get_current_blog_id() != $blog_id ) {
15976 switch_to_blog( $blog_id );
15977 $switched = true;
15978 }
15979
15980 if ( $site instanceof WP_Site ) {
15981 $url = $site->siteurl;
15982 $name = $site->blogname;
15983 $registration_date = $site->registered;
15984 } else {
15985 $url = self::get_unfiltered_site_url( $blog_id );
15986 $name = get_bloginfo( 'name' );
15987 }
15988 }
15989
15990 if ( empty( $registration_date ) && $load_registration ) {
15991 $blog_details = get_blog_details( $blog_id, false );
15992
15993 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15994 $registration_date = $blog_details->registered;
15995 }
15996 }
15997
15998 $info = array(
15999 'uid' => $this->get_anonymous_id( $blog_id ),
16000 'url' => $url,
16001 );
16002
16003 // Add these diagnostic information only if user allowed to track.
16004 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16005 $info = array_merge( $info, array(
16006 'title' => $name,
16007 'language' => self::get_sanitized_language(),
16008 ) );
16009 }
16010
16011 if ( is_numeric( $blog_id ) ) {
16012 $info['blog_id'] = $blog_id;
16013 }
16014
16015 if ( ! empty( $registration_date ) ) {
16016 $info[ 'registration_date' ] = $registration_date;
16017 }
16018
16019 if ( $switched ) {
16020 restore_current_blog();
16021 }
16022
16023 return $info;
16024 }
16025
16026 /**
16027 * Load the module's install based on the blog ID.
16028 *
16029 * @author Vova Feldman (@svovaf)
16030 * @since 2.0.0
16031 *
16032 * @param int|null $blog_id
16033 *
16034 * @return FS_Site
16035 */
16036 function get_install_by_blog_id( $blog_id = null ) {
16037 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16038 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16039
16040 if ( is_object( $install ) &&
16041 is_numeric( $install->id ) &&
16042 is_numeric( $install->user_id ) &&
16043 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16044 ) {
16045 // Load site.
16046 $install = clone $install;
16047 }
16048
16049 return $install;
16050 }
16051
16052 /**
16053 * Check if module is installed on a specified site.
16054 *
16055 * @author Vova Feldman (@svovaf)
16056 * @since 2.0.0
16057 *
16058 * @param int|null $blog_id
16059 *
16060 * @return bool
16061 */
16062 function is_installed_on_site( $blog_id = null ) {
16063 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16064 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16065
16066 return (
16067 is_object( $install ) &&
16068 is_numeric( $install->id ) &&
16069 is_numeric( $install->user_id ) &&
16070 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16071 );
16072 }
16073
16074 /**
16075 * Check if super-admin connected at least one site via the network opt-in.
16076 *
16077 * @author Vova Feldman (@svovaf)
16078 * @since 2.0.0
16079 *
16080 * @return bool
16081 */
16082 function is_network_registered() {
16083 if ( ! $this->_is_network_active ) {
16084 return false;
16085 }
16086
16087 return FS_User::is_valid_id( $this->_storage->network_user_id );
16088 }
16089
16090 /**
16091 * Returns the main user associated with the network.
16092 *
16093 * @author Vova Feldman (@svovaf)
16094 * @since 2.0.0
16095 *
16096 * @return FS_User
16097 */
16098 function get_network_user() {
16099 if ( ! $this->_is_network_active ) {
16100 return null;
16101 }
16102
16103 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
16104 self::_get_user_by_id( $this->_storage->network_user_id ) :
16105 null;
16106 }
16107
16108 /**
16109 * Returns the current context user or the network's main user.
16110 *
16111 * @author Vova Feldman (@svovaf)
16112 * @since 2.0.0
16113 *
16114 * @return FS_User
16115 */
16116 function get_current_or_network_user() {
16117 return ( $this->_user instanceof FS_User ) ?
16118 $this->_user :
16119 $this->get_network_user();
16120 }
16121
16122 /**
16123 * Returns the main install associated with the network.
16124 *
16125 * @author Vova Feldman (@svovaf)
16126 * @since 2.0.0
16127 *
16128 * @return FS_Site
16129 */
16130 function get_network_install() {
16131 if ( ! $this->_is_network_active ) {
16132 return null;
16133 }
16134
16135 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16136 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
16137 null;
16138 }
16139
16140 /**
16141 * Returns the blog ID that is associated with the main install.
16142 *
16143 * @author Leo Fajardo (@leorw)
16144 * @since 2.0.0
16145 *
16146 * @return int|null
16147 */
16148 function get_network_install_blog_id() {
16149 if ( ! $this->_is_network_active ) {
16150 return null;
16151 }
16152
16153 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16154 $this->_storage->network_install_blog_id :
16155 null;
16156 }
16157
16158 /**
16159 * Returns the current context install or the network's main install.
16160 *
16161 * @author Vova Feldman (@svovaf)
16162 * @since 2.0.0
16163 *
16164 * @return FS_Site
16165 */
16166 function get_current_or_network_install() {
16167 return ( $this->_site instanceof FS_Site ) ?
16168 $this->_site :
16169 $this->get_network_install();
16170 }
16171
16172 /**
16173 * Check if executing a site level action from the network level admin.
16174 *
16175 * @author Vova Feldman (@svovaf)
16176 * @since 2.0.0
16177 *
16178 * @return false|int If yes, return the requested blog ID.
16179 */
16180 private function is_network_level_site_specific_action() {
16181 if ( ! $this->_is_network_active ) {
16182 return false;
16183 }
16184
16185 if ( ! fs_is_network_admin() ) {
16186 return false;
16187 }
16188
16189 $blog_id = fs_request_get( 'blog_id', '' );
16190
16191 return is_numeric( $blog_id ) ? $blog_id : false;
16192 }
16193
16194 /**
16195 * Check if executing an action from the network level admin.
16196 *
16197 * @author Vova Feldman (@svovaf)
16198 * @since 2.0.0
16199 *
16200 * @return bool
16201 */
16202 private function is_network_level_action() {
16203 return ( $this->_is_network_active && fs_is_network_admin() );
16204 }
16205
16206 /**
16207 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16208 * 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.
16209 *
16210 * @author Vova Feldman (@svovaf)
16211 * @since 2.0.0
16212 *
16213 * @param int $context_blog_id
16214 */
16215 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16216 $this->_logger->entrance();
16217
16218 if ( $this->_is_network_active ) {
16219 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16220 $installs_map = $this->get_blog_install_map();
16221
16222 foreach ( $installs_map as $blog_id => $install ) {
16223 /**
16224 * @var FS_Site $install
16225 */
16226 if ( $context_blog_id == $blog_id ) {
16227 continue;
16228 }
16229
16230 if ( $install->user_id != $this->_storage->network_user_id ) {
16231 continue;
16232 }
16233
16234 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16235 $this->_storage->network_install_blog_id = $blog_id;
16236 break;
16237 }
16238 }
16239 }
16240
16241 if ( ! $this->is_registered() ) {
16242 return;
16243 }
16244
16245 if ( $this->is_sync_cron_scheduled() &&
16246 $context_blog_id == $this->get_sync_cron_blog_id()
16247 ) {
16248 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16249 }
16250
16251 if ( $this->is_install_sync_scheduled() &&
16252 $context_blog_id == $this->get_install_sync_cron_blog_id()
16253 ) {
16254 $this->schedule_install_sync( $context_blog_id );
16255 }
16256 }
16257
16258 /**
16259 * Executed after site deactivation, archive, or flag as spam.
16260 *
16261 * @author Vova Feldman (@svovaf)
16262 * @since 2.0.0
16263 *
16264 * @param int $context_blog_id
16265 */
16266 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16267 $this->_logger->entrance();
16268
16269 $install = $this->get_install_by_blog_id( $context_blog_id );
16270
16271 if ( ! is_object( $install ) ) {
16272 // Site not connected.
16273 return;
16274 }
16275
16276 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16277
16278 if ( ! $this->is_registered() ) {
16279 return;
16280 }
16281
16282 $current_blog_id = get_current_blog_id();
16283
16284 $this->switch_to_blog( $context_blog_id );
16285
16286 // Send deactivation event.
16287 $this->sync_install( array(
16288 'is_active' => false,
16289 ) );
16290
16291 $this->switch_to_blog( $current_blog_id );
16292 }
16293
16294 /**
16295 * Executed after site deletion.
16296 *
16297 * @author Vova Feldman (@svovaf)
16298 * @since 2.0.0
16299 *
16300 * @param int $context_blog_id
16301 * @param bool $drop True if site's database tables should be dropped. Default is false.
16302 */
16303 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16304 $this->_logger->entrance();
16305
16306 $install = $this->get_install_by_blog_id( $context_blog_id );
16307
16308 if ( ! is_object( $install ) ) {
16309 // Site not connected.
16310 return;
16311 }
16312
16313 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16314
16315 if ( ! $this->is_registered() ) {
16316 return;
16317 }
16318
16319 $current_blog_id = get_current_blog_id();
16320
16321 $this->switch_to_blog( $context_blog_id );
16322
16323 if ( $drop ) {
16324 // Delete install if dropping site DB.
16325 $this->delete_account_event();
16326 } else {
16327 // Send deactivation event.
16328 $this->sync_install( array(
16329 'is_active' => false,
16330 ) );
16331 }
16332
16333 $this->switch_to_blog( $current_blog_id );
16334 }
16335
16336 /**
16337 * Executed after site deletion, called from wp_delete_site
16338 *
16339 * @author Dario Curvino (@dudo)
16340 * @since 2.5.0
16341 *
16342 * @param WP_Site $old_site
16343 */
16344 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16345 $this->_logger->entrance();
16346
16347 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16348 }
16349
16350 /**
16351 * Executed after site re-activation.
16352 *
16353 * @author Vova Feldman (@svovaf)
16354 * @since 2.0.0
16355 *
16356 * @param int $context_blog_id
16357 */
16358 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16359 $this->_logger->entrance();
16360
16361 $install = $this->get_install_by_blog_id( $context_blog_id );
16362
16363 if ( ! is_object( $install ) ) {
16364 // Site not connected.
16365 return;
16366 }
16367
16368 if ( ! self::is_site_active( $context_blog_id ) ) {
16369 // Site not yet active (can be in spam mode, archived, deleted...).
16370 return;
16371 }
16372
16373 $current_blog_id = get_current_blog_id();
16374
16375 $this->switch_to_blog( $context_blog_id );
16376
16377 // Send re-activation event.
16378 $this->sync_install( array(
16379 'is_active' => true,
16380 ) );
16381
16382 $this->switch_to_blog( $current_blog_id );
16383 }
16384
16385 #endregion Multisite
16386
16387 /**
16388 * @author Leo Fajardo (@leorw)
16389 *
16390 * @param string $path
16391 * @param string $scheme
16392 * @param bool $network
16393 *
16394 * @return string
16395 */
16396 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16397 return ( $this->_is_network_active && $network ) ?
16398 network_admin_url( $path, $scheme ) :
16399 admin_url( $path, $scheme );
16400 }
16401
16402 /**
16403 * Check if currently in a specified admin page.
16404 *
16405 * @author Vova Feldman (@svovaf)
16406 * @since 1.2.2.7
16407 *
16408 * @param string $page
16409 *
16410 * @return bool
16411 */
16412 function is_admin_page( $page ) {
16413 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16414 }
16415
16416 /**
16417 * Check if currently in the product's main admin page.
16418 *
16419 * @author Vova Feldman (@svovaf)
16420 * @since 2.3.1
16421 *
16422 * @return bool
16423 */
16424 function is_main_admin_page() {
16425 return $this->is_admin_page( '' );
16426 }
16427
16428 /**
16429 * Get module's main admin setting page URL.
16430 *
16431 * @author Vova Feldman (@svovaf)
16432 * @since 1.2.2.7
16433 *
16434 * @return string
16435 */
16436 function main_menu_url() {
16437 return $this->_menu->main_menu_url();
16438 }
16439
16440 /**
16441 * Check if currently on the theme's setting page or
16442 * on any of the Freemius added pages (via tabs).
16443 *
16444 * @author Vova Feldman (@svovaf)
16445 * @since 1.2.2.7
16446 *
16447 * @return bool
16448 *
16449 * @deprecated Please use is_product_settings_page() instead;
16450 */
16451 function is_theme_settings_page() {
16452 return $this->is_product_settings_page();
16453 }
16454
16455 /**
16456 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16457 *
16458 * @author Vova Feldman (@svovaf)
16459 * @since 1.2.2.7
16460 *
16461 * @return bool
16462 */
16463 function is_product_settings_page() {
16464 $page = fs_request_get( 'page', '', 'get' );
16465 $menu_slug = $this->_menu->get_slug();
16466
16467 if ( $page === $menu_slug ) {
16468 return true;
16469 }
16470
16471 return fs_starts_with(
16472 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16473 $page,
16474 ( $menu_slug . '-' )
16475 );
16476 }
16477
16478 /**
16479 * Plugin's account page + sync license URL.
16480 *
16481 * @author Vova Feldman (@svovaf)
16482 * @since 1.1.9.1
16483 *
16484 * @param bool|number $plugin_id
16485 * @param bool $add_action_nonce
16486 * @param array $params
16487 *
16488 * @return string
16489 */
16490 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16491 if ( is_numeric( $plugin_id ) ) {
16492 $params['plugin_id'] = $plugin_id;
16493 }
16494
16495 return $this->get_account_url(
16496 $this->get_unique_affix() . '_sync_license',
16497 $params,
16498 $add_action_nonce
16499 );
16500 }
16501
16502 /**
16503 * Plugin's account URL.
16504 *
16505 * @author Vova Feldman (@svovaf)
16506 * @since 1.0.4
16507 *
16508 * @param bool|string $action
16509 * @param array $params
16510 *
16511 * @param bool $add_action_nonce
16512 *
16513 * @return string
16514 */
16515 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16516 if ( is_string( $action ) ) {
16517 $params['fs_action'] = $action;
16518 }
16519
16520 self::require_pluggable_essentials();
16521
16522 return ( $add_action_nonce && is_string( $action ) ) ?
16523 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16524 $this->_get_admin_page_url( 'account', $params );
16525 }
16526
16527 /**
16528 * @author Vova Feldman (@svovaf)
16529 * @since 1.2.0
16530 *
16531 * @param string $tab
16532 * @param bool $action
16533 * @param array $params
16534 * @param bool $add_action_nonce
16535 *
16536 * @return string
16537 *
16538 * @uses get_account_url()
16539 */
16540 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16541 $params['tab'] = $tab;
16542
16543 return $this->get_account_url( $action, $params, $add_action_nonce );
16544 }
16545
16546 /**
16547 * Plugin's account URL.
16548 *
16549 * @author Vova Feldman (@svovaf)
16550 * @since 1.0.4
16551 *
16552 * @param bool|string $topic
16553 * @param bool|string $message
16554 * @param bool|string $summary Since 2.5.1.
16555 *
16556 * @return string
16557 */
16558 function contact_url( $topic = false, $message = false, $summary = false ) {
16559 $params = array();
16560 if ( is_string( $topic ) ) {
16561 $params['topic'] = $topic;
16562 }
16563 if ( is_string( $message ) ) {
16564 $params['message'] = $message;
16565 }
16566
16567 if ( is_string( $summary ) ) {
16568 $params['summary'] = $summary;
16569 }
16570
16571 if ( $this->is_addon() ) {
16572 $params['addon_id'] = $this->get_id();
16573
16574 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16575 } else {
16576 return $this->_get_admin_page_url( 'contact', $params );
16577 }
16578 }
16579
16580 /**
16581 * Add-on direct info URL.
16582 *
16583 * @author Vova Feldman (@svovaf)
16584 * @since 1.1.0
16585 *
16586 * @param string $slug
16587 *
16588 * @return string
16589 */
16590 function addon_url( $slug ) {
16591 return $this->_get_admin_page_url( 'addons', array(
16592 'slug' => $slug
16593 ) );
16594 }
16595
16596 /**
16597 * Add-ons URL.
16598 *
16599 * @author Vova Feldman (@svovaf)
16600 * @since 2.4.5
16601 *
16602 * @return string
16603 */
16604 function get_addons_url() {
16605 return $this->_get_admin_page_url( 'addons' );
16606 }
16607
16608 /* Logger
16609 ------------------------------------------------------------------------------------------------------------------*/
16610 /**
16611 * @param string $id
16612 * @param bool $prefix_slug
16613 *
16614 * @return FS_Logger
16615 */
16616 function get_logger( $id = '', $prefix_slug = true ) {
16617 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16618 }
16619
16620 /**
16621 * Note: This method is used externally so don't delete it.
16622 *
16623 * @param $id
16624 * @param bool $load_options
16625 * @param bool $prefix_slug
16626 *
16627 * @return FS_Option_Manager
16628 */
16629 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16630 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16631 }
16632
16633 /* Security
16634 ------------------------------------------------------------------------------------------------------------------*/
16635 private static function _encrypt( $str ) {
16636 if ( is_null( $str ) ) {
16637 return null;
16638 }
16639
16640 /**
16641 * The encrypt/decrypt functions are used to protect
16642 * the user from messing up with some of the sensitive
16643 * data stored for the module as a JSON in the database.
16644 *
16645 * I used the same suggested hack by the theme review team.
16646 * For more details, look at the function `Base64UrlDecode()`
16647 * in `./sdk/FreemiusBase.php`.
16648 *
16649 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16650 *
16651 * @author Vova Feldman (@svovaf)
16652 * @since 1.2.2
16653 */
16654 $fn = 'base64' . '_encode';
16655
16656 return $fn( $str );
16657 }
16658
16659 static function _decrypt( $str ) {
16660 if ( is_null( $str ) ) {
16661 return null;
16662 }
16663
16664 /**
16665 * The encrypt/decrypt functions are used to protect
16666 * the user from messing up with some of the sensitive
16667 * data stored for the module as a JSON in the database.
16668 *
16669 * I used the same suggested hack by the theme review team.
16670 * For more details, look at the function `Base64UrlDecode()`
16671 * in `./sdk/FreemiusBase.php`.
16672 *
16673 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16674 *
16675 * @author Vova Feldman (@svovaf)
16676 * @since 1.2.2
16677 */
16678 $fn = 'base64' . '_decode';
16679
16680 return $fn( $str );
16681 }
16682
16683 /**
16684 * @author Vova Feldman (@svovaf)
16685 * @since 1.0.5
16686 *
16687 * @param FS_Entity $entity
16688 *
16689 * @return FS_Entity Return an encrypted clone entity.
16690 */
16691 private static function _encrypt_entity( FS_Entity $entity ) {
16692 $clone = clone $entity;
16693 $props = get_object_vars( $entity );
16694
16695 foreach ( $props as $key => $val ) {
16696 $clone->{$key} = self::_encrypt( $val );
16697 }
16698
16699 return $clone;
16700 }
16701
16702 /**
16703 * @author Vova Feldman (@svovaf)
16704 * @since 1.0.5
16705 *
16706 * @param FS_Entity $entity
16707 *
16708 * @return FS_Entity Return an decrypted clone entity.
16709 */
16710 private static function decrypt_entity( FS_Entity $entity ) {
16711 $clone = clone $entity;
16712 $props = get_object_vars( $entity );
16713
16714 foreach ( $props as $key => $val ) {
16715 $clone->{$key} = self::_decrypt( $val );
16716 }
16717
16718 return $clone;
16719 }
16720
16721 /**
16722 * @author Vova Feldman (@svovaf)
16723 * @since 1.0.7
16724 *
16725 * @param string $email
16726 *
16727 * @return FS_User|false
16728 */
16729 public static function _get_user_by_email( $email ) {
16730 self::$_static_logger->entrance();
16731
16732 $email = trim( strtolower( $email ) );
16733
16734 $users = self::get_all_users();
16735
16736 if ( is_array( $users ) ) {
16737 foreach ( $users as $user ) {
16738 if ( $email === trim( strtolower( $user->email ) ) ) {
16739 return $user;
16740 }
16741 }
16742 }
16743
16744 return false;
16745 }
16746
16747 #----------------------------------------------------------------------------------
16748 #region Account (Loading, Updates & Activation)
16749 #----------------------------------------------------------------------------------
16750
16751 /***
16752 * Load account information (user + site).
16753 *
16754 * @author Vova Feldman (@svovaf)
16755 * @since 1.0.1
16756 */
16757 private function _load_account() {
16758 $this->_logger->entrance();
16759
16760 $this->do_action( 'before_account_load' );
16761
16762 $users = self::get_all_users();
16763 $plans = self::get_all_plans( $this->_module_type );
16764
16765 if ( $this->_logger->is_on() && is_admin() ) {
16766 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16767 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16768 }
16769
16770 $site = fs_is_network_admin() ?
16771 $this->get_network_install() :
16772 $this->get_install_by_blog_id();
16773
16774 if ( fs_is_network_admin() &&
16775 $this->is_network_active() &&
16776 ! is_object( $site ) &&
16777 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16778 ) {
16779 $first_install = $this->find_first_install();
16780
16781 if ( is_null( $first_install ) ) {
16782 unset( $this->_storage->network_install_blog_id );
16783 } else {
16784 $site = $first_install['install'];
16785 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16786 }
16787 }
16788
16789 if ( is_object( $site ) &&
16790 is_numeric( $site->id ) &&
16791 is_numeric( $site->user_id ) &&
16792 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16793 ) {
16794 // Load site.
16795 $this->_site = $site;
16796 }
16797
16798 $user = null;
16799 if ( fs_is_network_admin() && $this->_is_network_active ) {
16800 $user = $this->get_network_user();
16801 }
16802
16803 if ( is_object( $user ) ) {
16804 $this->_user = clone $user;
16805 } else if ( $this->_site ) {
16806 $user = self::_get_user_by_id( $this->_site->user_id );
16807
16808 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16809 /**
16810 * Try to load the previous owner. This recovery is used for the following use-case:
16811 * 1. Opt-in
16812 * 2. Cloning site1 to site2
16813 * 3. Ownership switch in site1 (same applies for site2)
16814 * 4. Install data sync on site2
16815 * 5. Now site2's install is associated with the new owner which does not exists locally.
16816 */
16817 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16818 }
16819
16820 if ( ! is_object( $user ) ) {
16821 /**
16822 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16823 */
16824 if (
16825 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16826 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16827 ) {
16828 $user = $this->sync_user_by_current_install();
16829 } else {
16830 return;
16831 }
16832
16833 if ( is_object( $user ) ) {
16834 $this->_storage->user_was_recovered_from_install = true;
16835 } else {
16836 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16837 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16838 1;
16839
16840 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16841 $this->delete_current_install( false );
16842 } else {
16843 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16844
16845 return;
16846 }
16847 }
16848 }
16849
16850 $this->_user = ( $user instanceof FS_User ) ?
16851 clone $user :
16852 null;
16853 }
16854
16855 if ( is_object( $this->_user ) ) {
16856 // Load licenses.
16857 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16858 }
16859
16860 if ( is_object( $this->_site ) ) {
16861 // Load plans.
16862 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16863 $plans[ $this->_slug ] :
16864 array();
16865
16866 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16867 $this->_sync_plans();
16868 } else {
16869 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16870 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16871 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16872 } else {
16873 unset( $this->_plans[ $i ] );
16874 }
16875 }
16876 }
16877
16878 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16879
16880 if ( $this->_site->version != $this->get_plugin_version() ) {
16881 // If stored install version is different than current installed plugin version,
16882 // then update plugin version event.
16883 $this->update_plugin_version_event();
16884 }
16885 }
16886
16887 if ( true === $this->_storage->require_license_activation &&
16888 ! fs_request_get_bool( 'require_license', true )
16889 ) {
16890 $this->_storage->require_license_activation = false;
16891 }
16892
16893 if ( $this->is_theme() ) {
16894 $this->_register_account_hooks();
16895 }
16896
16897 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16898 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16899 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16900 }
16901
16902 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16903 $this->send_pending_clone_update_once();
16904 }
16905 }
16906
16907 /**
16908 * Special user recovery mechanism.
16909 *
16910 * @author Vova Feldman (@svovaf)
16911 * @since 2.0.0
16912 *
16913 * @param number|null $site_user_id
16914 *
16915 * @return \FS_User|mixed
16916 */
16917 private function sync_user_by_current_install( $site_user_id = null ) {
16918 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16919 $site_user_id :
16920 $this->_site->user_id;
16921
16922 $api = $this->get_api_site_scope();
16923
16924 $uid = $this->get_anonymous_id();
16925 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16926
16927 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16928
16929 if ( $this->is_api_result_entity( $result ) ) {
16930 $user = new FS_User( $result );
16931 $this->_user = $user;
16932 $this->_store_user();
16933
16934 return $user;
16935 }
16936
16937 $error_code = FS_Api::get_error_code( $result );
16938
16939 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16940 /**
16941 * Those API errors will continue coming and are not recoverable with the
16942 * current site's data. Therefore, extend the API call's cached result to 7 days.
16943 */
16944 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16945 }
16946
16947 return $result;
16948 }
16949
16950 /**
16951 * @author Vova Feldman (@svovaf)
16952 * @since 1.0.1
16953 *
16954 * @param FS_User $user
16955 * @param FS_Site $site
16956 * @param bool|array $plans
16957 */
16958 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16959 $site->user_id = $user->id;
16960
16961 $this->_site = $site;
16962 $this->_user = $user;
16963 if ( false !== $plans ) {
16964 $this->_plans = $plans;
16965 }
16966
16967 $this->send_install_update();
16968
16969 $this->_store_account();
16970
16971 }
16972
16973 /**
16974 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16975 * Each version is trimmed after the 16th char.
16976 *
16977 * @author Vova Feldman (@svovaf)
16978 * @since 2.2.1
16979 *
16980 * @return array
16981 */
16982 private function get_versions() {
16983 $versions = array();
16984 $versions['sdk_version'] = $this->version;
16985
16986 // Collect these diagnostic information only if it's allowed.
16987 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16988 $versions['platform_version'] = get_bloginfo( 'version' );
16989 $versions['programming_language_version'] = phpversion();
16990 }
16991
16992 foreach ( $versions as $k => $version ) {
16993 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
16994 }
16995
16996 return $versions;
16997 }
16998
16999 /**
17000 * Get sanitized site language.
17001 *
17002 * @param string $language
17003 * @param int $max_len
17004 *
17005 * @since 2.5.1
17006 * @author Vova Feldman (@svovaf)
17007 *
17008 * @return string
17009 */
17010 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
17011 if ( empty( $language ) ) {
17012 $language = get_bloginfo( 'language' );
17013 }
17014
17015 return substr( $language, 0, $max_len );
17016 }
17017
17018 /**
17019 * Get core version stripped from pre-release and build.
17020 *
17021 * @since 2.5.1
17022 * @author Vova Feldman (@svovaf)
17023 *
17024 * @param string $version
17025 * @param int $parts
17026 * @param int $max_len
17027 * @param bool $include_pre_release
17028 *
17029 * @return string
17030 */
17031 private static function get_core_version(
17032 $version,
17033 $parts = 3,
17034 $max_len = self::VERSION_MAX_CHARS,
17035 $include_pre_release = false
17036 ) {
17037 if ( empty( $version ) ) {
17038 // Version is empty.
17039 return '';
17040 }
17041
17042 if ( is_numeric( $version ) ) {
17043 $is_float_version = is_float( $version );
17044
17045 $version = (string) $version;
17046
17047 /**
17048 * 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.
17049 */
17050 if ( $is_float_version && false === strpos( $version, '.' ) ) {
17051 $version .= '.0';
17052 }
17053 }
17054
17055 if ( ! is_string( $version ) ) {
17056 return '';
17057 }
17058
17059 if ( $parts < 1 ) {
17060 return '';
17061 }
17062
17063 $pre_release_regex = $include_pre_release ?
17064 '(\-(alpha|beta|RC)([0-9]+)?)?' :
17065 '';
17066
17067 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
17068 // Version is not starting with a digit.
17069 return '';
17070 }
17071
17072 return substr( $matches[1], 0, $max_len );
17073 }
17074
17075 /**
17076 * @param string $prop
17077 * @param mixed $val
17078 *
17079 * @return mixed
17080 *@author Vova Feldman (@svovaf)
17081 *
17082 * @since 2.5.1
17083 */
17084 private static function get_api_sanitized_property( $prop, $val ) {
17085 if ( ! is_string( $val ) || empty( $val ) ) {
17086 return $val;
17087 }
17088
17089 switch ( $prop ) {
17090 case 'programming_language_version':
17091 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
17092 return self::get_core_version( $val );
17093 case 'platform_version':
17094 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
17095 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
17096 case 'sdk_version':
17097 // Get the exact SDK version, which can have up to 4 parts.
17098 return self::get_core_version( $val, 4 );
17099 case 'version':
17100 // Get the entire version but just limited in length.
17101 return substr( $val, 0, self::VERSION_MAX_CHARS );
17102 case 'language':
17103 return self::get_sanitized_language( $val );
17104 default:
17105 return $val;
17106 }
17107 }
17108
17109 /**
17110 * @author Leo Fajardo (@leorw)
17111 * @since 2.3.0
17112 *
17113 * @return bool
17114 */
17115 function has_beta_update() {
17116 return (
17117 ! empty( $this->_storage->beta_data ) &&
17118 ( true === $this->_storage->beta_data['is_beta'] ) &&
17119 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
17120 );
17121 }
17122
17123 /**
17124 * @author Leo Fajardo (@leorw)
17125 * @since 2.3.0
17126 *
17127 * @return bool
17128 */
17129 function is_beta() {
17130 return (
17131 ! empty( $this->_storage->beta_data ) &&
17132 ( true === $this->_storage->beta_data['is_beta'] ) &&
17133 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
17134 );
17135 }
17136
17137 /**
17138 * @author Vova Feldman (@svovaf)
17139 * @since 1.1.7.4
17140 *
17141 * @param array $override_with
17142 * @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.
17143 *
17144 * @return array
17145 */
17146 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
17147 $this->_logger->entrance();
17148
17149 $current_user = self::_get_current_wp_user();
17150
17151 $activation_action = $this->get_unique_affix() . '_activate_new';
17152 $return_url = $this->is_anonymous() ?
17153 // If skipped already, then return to the account page.
17154 $this->get_account_url( $activation_action, array(), false ) :
17155 // Return to the module's main page.
17156 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
17157
17158 $versions = $this->get_versions();
17159
17160 $params = array_merge( $versions, array(
17161 'user_firstname' => $current_user->user_firstname,
17162 'user_lastname' => $current_user->user_lastname,
17163 'user_email' => $current_user->user_email,
17164 'plugin_slug' => $this->_slug,
17165 'plugin_id' => $this->get_id(),
17166 'plugin_public_key' => $this->get_public_key(),
17167 'plugin_version' => $this->get_plugin_version(),
17168 'return_url' => fs_nonce_url( $return_url, $activation_action ),
17169 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
17170 'account',
17171 array( 'fs_action' => 'sync_user' )
17172 ), 'sync_user' ),
17173 'is_premium' => $this->is_premium(),
17174 'is_active' => true,
17175 'is_uninstalled' => false,
17176 'is_localhost' => WP_FS__IS_LOCALHOST,
17177 ) );
17178
17179 if ( $this->is_addon() ) {
17180 $parent_fs = $this->get_parent_instance();
17181
17182 $params['parent_plugin_slug'] = $parent_fs->_slug;
17183 $params['parent_plugin_id'] = $parent_fs->get_id();
17184 }
17185
17186 if ( true === $network_level_or_blog_id ) {
17187 if ( ! isset( $override_with['sites'] ) ) {
17188 $params['sites'] = $this->get_sites_for_network_level_optin();
17189 }
17190 } else {
17191 $site = is_numeric( $network_level_or_blog_id ) ?
17192 array( 'blog_id' => $network_level_or_blog_id ) :
17193 null;
17194
17195 $site = $this->get_site_info( $site );
17196
17197 $diagnostic_info = array();
17198 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17199 $diagnostic_info = array(
17200 'site_name' => $site['title'],
17201 'language' => self::get_sanitized_language( $site['language'] ),
17202 );
17203 }
17204
17205 $params = array_merge( $params, $diagnostic_info, array(
17206 'site_uid' => $site['uid'],
17207 'site_url' => $site['url'],
17208 ) );
17209 }
17210
17211 if ( $this->is_pending_activation() &&
17212 ! empty( $this->_storage->pending_license_key )
17213 ) {
17214 $params['license_key'] = $this->_storage->pending_license_key;
17215 }
17216
17217 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17218 // Even though rand() is known for its security issues,
17219 // the timestamp adds another layer of protection.
17220 // It would be very hard for an attacker to get the secret key form here.
17221 // Plus, this should never run in production since the secret should never
17222 // be included in the production version.
17223 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17224 $params['salt'] = md5( uniqid( rand() ) );
17225 $params['secure'] = md5(
17226 $params['ts'] .
17227 $params['salt'] .
17228 $this->get_secret_key()
17229 );
17230 }
17231
17232 if ( is_multisite() && function_exists( 'get_network' ) ) {
17233 $params['network_uid'] = $this->get_anonymous_network_id();
17234 }
17235
17236 return array_merge( $params, $override_with );
17237 }
17238
17239 /**
17240 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17241 * 2. If there was an API error, return the API result.
17242 *
17243 * @author Vova Feldman (@svovaf)
17244 * @since 1.1.7.4
17245 *
17246 * @param string|bool $email
17247 * @param string|bool $first
17248 * @param string|bool $last
17249 * @param string|bool $license_key
17250 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17251 * In this case, the user and site info will be sent to the server but no
17252 * data will be saved to the WP installation's database.
17253 * @param number|bool $trial_plan_id
17254 * @param bool $is_disconnected Whether to opt in without tracking.
17255 * @param null|bool $is_marketing_allowed
17256 * @param array $sites If network-level opt-in, an array of containing details of sites.
17257 * @param bool $redirect
17258 *
17259 * @return string|object
17260 * @use WP_Error
17261 */
17262 function opt_in(
17263 $email = false,
17264 $first = false,
17265 $last = false,
17266 $license_key = false,
17267 $is_uninstall = false,
17268 $trial_plan_id = false,
17269 $is_disconnected = false,
17270 $is_marketing_allowed = null,
17271 $sites = array(),
17272 $redirect = true
17273 ) {
17274 $this->_logger->entrance();
17275
17276 if ( false === $email ) {
17277 $current_user = self::_get_current_wp_user();
17278 $email = $current_user->user_email;
17279 }
17280
17281 /**
17282 * @since 1.2.1 If activating with license key, ignore the context-user
17283 * since the user will be automatically loaded from the license.
17284 */
17285 if ( empty( $license_key ) ) {
17286 // Clean up pending license if opt-ing in again.
17287 $this->_storage->remove( 'pending_license_key' );
17288
17289 if ( ! $is_uninstall ) {
17290 $fs_user = Freemius::_get_user_by_email( $email );
17291 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17292 return $this->install_with_user(
17293 $fs_user,
17294 false,
17295 $trial_plan_id,
17296 $redirect,
17297 true,
17298 $sites
17299 );
17300 }
17301 }
17302 }
17303
17304 $user_info = array();
17305 if ( ! empty( $email ) ) {
17306 $user_info['user_email'] = $email;
17307 }
17308 if ( ! empty( $first ) ) {
17309 $user_info['user_firstname'] = $first;
17310 }
17311 if ( ! empty( $last ) ) {
17312 $user_info['user_lastname'] = $last;
17313 }
17314
17315 if ( ! empty( $sites ) ) {
17316 $is_network = true;
17317
17318 $user_info['sites'] = $sites;
17319 } else {
17320 $is_network = false;
17321 }
17322
17323 $params = $this->get_opt_in_params( $user_info, $is_network );
17324
17325 $filtered_license_key = false;
17326 if ( is_string( $license_key ) ) {
17327 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17328 $params['license_key'] = $filtered_license_key;
17329 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17330 $params['trial_plan_id'] = $trial_plan_id;
17331 }
17332
17333 if ( $is_uninstall ) {
17334 $params['uninstall_params'] = array(
17335 'reason_id' => $this->_storage->uninstall_reason->id,
17336 'reason_info' => $this->_storage->uninstall_reason->info
17337 );
17338 }
17339
17340 if ( isset( $params['license_key'] ) ) {
17341 $fs_user = Freemius::_get_user_by_email( $email );
17342
17343 if ( is_object( $fs_user ) ) {
17344 /**
17345 * If opting in with a context license and the context WP Admin user already opted in
17346 * before from the current site, add the user context security params to avoid the
17347 * unnecessary email activation when the context license is owned by the same context user.
17348 *
17349 * @author Leo Fajardo (@leorw)
17350 * @since 1.2.3
17351 */
17352 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17353 $fs_user,
17354 false,
17355 'install_with_existing_user'
17356 ) );
17357 }
17358 }
17359
17360 if ( is_bool( $is_marketing_allowed ) ) {
17361 $params['is_marketing_allowed'] = $is_marketing_allowed;
17362 }
17363
17364 $params['is_disconnected'] = $is_disconnected;
17365 $params['format'] = 'json';
17366 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17367 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17368
17369 $request = array(
17370 'method' => 'POST',
17371 'body' => $params,
17372 'timeout' => 60,
17373 );
17374
17375 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17376 $response = self::safe_remote_post( $url, $request );
17377
17378 if ( is_wp_error( $response ) ) {
17379 /**
17380 * @var WP_Error $response
17381 */
17382 $result = new stdClass();
17383
17384 $error_code = $response->get_error_code();
17385 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17386
17387 $result->error = (object) array(
17388 'type' => $error_type,
17389 'message' => $response->get_error_message(),
17390 'code' => $error_code,
17391 'http' => 402
17392 );
17393
17394 $this->maybe_modify_api_curl_error_message( $result );
17395
17396 if ( FS_Api::is_blocked( $result ) ) {
17397 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17398 }
17399
17400 $is_connected = null;
17401
17402 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17403 $this->skip_connection( fs_is_network_admin() );
17404
17405 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17406 }
17407
17408 $this->update_connectivity_info( $is_connected );
17409
17410 return $result;
17411 }
17412
17413 $this->update_connectivity_info( true );
17414
17415 // Module is being uninstalled, don't handle the returned data.
17416 if ( $is_uninstall ) {
17417 return true;
17418 }
17419
17420 /**
17421 * 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.
17422 *
17423 * @author Vova Feldman (@svovaf)
17424 * @since 1.2.3
17425 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17426 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17427 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17428 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17429 */
17430 $decoded = is_string( $response['body'] ) ?
17431 json_decode( $response['body'] ) :
17432 null;
17433
17434 if ( empty( $decoded ) ) {
17435 return false;
17436 }
17437
17438 if ( ! $this->is_api_result_object( $decoded ) ) {
17439 if ( ! empty( $params['license_key'] ) ) {
17440 // Pass the fully entered license key to the failure handler.
17441 $params['license_key'] = $license_key;
17442 }
17443
17444 return $is_uninstall ?
17445 $decoded :
17446 $this->apply_filters( 'after_install_failure', $decoded, $params );
17447 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17448 if ( $is_network ) {
17449 $site_ids = array();
17450 foreach ( $sites as $site ) {
17451 $site_ids[] = $site['blog_id'];
17452 }
17453
17454 /**
17455 * Store the sites so that they can be installed once the user has clicked on the activation link
17456 * in the email.
17457 *
17458 * @author Leo Fajardo (@leorw)
17459 */
17460 $this->_storage->pending_sites_info = array(
17461 'blog_ids' => $site_ids,
17462 'license_key' => $license_key,
17463 'trial_plan_id' => $trial_plan_id
17464 );
17465 }
17466
17467 // Pending activation, add message.
17468 return $this->set_pending_confirmation(
17469 ( isset( $decoded->email ) ?
17470 $decoded->email :
17471 true ),
17472 false,
17473 $filtered_license_key,
17474 ! empty( $params['trial_plan_id'] ),
17475 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17476 );
17477 } else if ( isset( $decoded->install_secret_key ) ) {
17478 return $this->install_with_new_user(
17479 $decoded->user_id,
17480 $decoded->user_public_key,
17481 $decoded->user_secret_key,
17482 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17483 $decoded->is_marketing_allowed :
17484 null ),
17485 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17486 $decoded->is_extensions_tracking_allowed :
17487 null ),
17488 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17489 $decoded->is_diagnostic_tracking_allowed :
17490 null ),
17491 $decoded->install_id,
17492 $decoded->install_public_key,
17493 $decoded->install_secret_key,
17494 false
17495 );
17496 } else if ( is_array( $decoded->installs ) ) {
17497 return $this->install_many_with_new_user(
17498 $decoded->user_id,
17499 $decoded->user_public_key,
17500 $decoded->user_secret_key,
17501 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17502 $decoded->is_marketing_allowed :
17503 null ),
17504 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17505 $decoded->is_extensions_tracking_allowed :
17506 null ),
17507 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17508 $decoded->is_diagnostic_tracking_allowed :
17509 null ),
17510 $decoded->installs,
17511 false
17512 );
17513 }
17514
17515 return $decoded;
17516 }
17517
17518 /**
17519 * Set user and site identities.
17520 *
17521 * @author Vova Feldman (@svovaf)
17522 * @since 1.0.9
17523 *
17524 * @param FS_User $user
17525 * @param FS_Site $site
17526 * @param bool $redirect
17527 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17528 * redirect (or return a URL) to the account page with a special parameter to
17529 * trigger the auto installation processes.
17530 *
17531 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17532 */
17533 function setup_account(
17534 FS_User $user,
17535 FS_Site $site,
17536 $redirect = true,
17537 $auto_install = false
17538 ) {
17539 return $this->setup_network_account(
17540 $user,
17541 array( $site ),
17542 $redirect,
17543 $auto_install,
17544 false
17545 );
17546 }
17547
17548 /**
17549 * Set user and site identities.
17550 *
17551 * @author Vova Feldman (@svovaf)
17552 * @since 2.0.0
17553 *
17554 * @param FS_User $user
17555 * @param FS_Site[] $installs
17556 * @param bool $redirect
17557 * @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.
17558 * @param bool $is_network_level_opt_in
17559 *
17560 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17561 */
17562 function setup_network_account(
17563 FS_User $user,
17564 array $installs,
17565 $redirect = true,
17566 $auto_install = false,
17567 $is_network_level_opt_in = true
17568 ) {
17569 $first_install = $installs[0];
17570
17571 $this->_user = $user;
17572 $this->_site = $first_install;
17573
17574 $this->_sync_plans();
17575
17576 if ( $this->_storage->handle_gdpr_admin_notice &&
17577 $this->should_handle_gdpr_admin_notice() &&
17578 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17579 ) {
17580 /**
17581 * Clear user lock after an opt-in.
17582 */
17583 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17584 FS_User_Lock::instance()->unlock();
17585 }
17586
17587 if ( 1 < count( $installs ) ) {
17588 // Only network level opt-in can have more than one install.
17589 $is_network_level_opt_in = true;
17590 }
17591
17592 $this->update_connectivity_info( true );
17593
17594 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17595 // If Freemius was OFF before, turn it on.
17596 $this->turn_on();
17597
17598 $this->handle_account_connection(
17599 $installs,
17600 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17601 );
17602
17603 if ( is_numeric( $first_install->license_id ) ) {
17604 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17605 }
17606
17607 $this->_admin_notices->remove_sticky( 'connect_account' );
17608
17609 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17610 $this->clear_pending_activation_mode();
17611
17612 if ( ! $this->is_paying_or_trial() ) {
17613 $this->_admin_notices->add_sticky(
17614 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17615 'activation_complete'
17616 );
17617 }
17618 }
17619
17620 if ( $this->is_paying_or_trial() ) {
17621 if ( ! $this->is_premium() ||
17622 ! $this->has_premium_version() ||
17623 ! $this->has_settings_menu()
17624 ) {
17625 if ( $this->is_paying() ) {
17626 $this->add_complete_upgrade_instructions_notice(
17627 sprintf(
17628 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17629 $this->get_plan_title()
17630 ),
17631 'plan_upgraded'
17632 );
17633 } else {
17634 $trial_plan = $this->get_trial_plan();
17635
17636 $this->add_complete_upgrade_instructions_notice(
17637 sprintf(
17638 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17639 '<i>' . $this->get_plugin_name() . '</i>'
17640 ),
17641 'trial_started',
17642 $trial_plan->title
17643 );
17644 }
17645 }
17646
17647 $this->_admin_notices->remove_sticky( array(
17648 'trial_promotion',
17649 ) );
17650 }
17651
17652 $plugin_id = fs_request_get( 'plugin_id', false );
17653
17654 // Store activation time ONLY for plugins & themes (not add-ons).
17655 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17656 if ( empty( $this->_storage->activation_timestamp ) ) {
17657 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17658 }
17659 }
17660
17661 $next_page = '';
17662
17663 $extra = array();
17664 if ( $auto_install ) {
17665 $extra['auto_install'] = 'true';
17666 }
17667
17668 if ( is_numeric( $plugin_id ) ) {
17669 /**
17670 * @author Leo Fajardo (@leorw)
17671 * @since 1.2.1.6
17672 *
17673 * Also sync the license after an anonymous user subscribes.
17674 */
17675 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17676 // Add-on was installed - sync license right after install.
17677 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17678 }
17679 } else {
17680 /**
17681 * @author Vova Feldman (@svovaf)
17682 * @since 1.1.9 If site installed with a valid license, sync license.
17683 */
17684 if ( $this->is_paying() ) {
17685 $this->_sync_plugin_license(
17686 true,
17687 // Installs data is already synced in the beginning of this method directly or via _set_account().
17688 false
17689 );
17690 }
17691
17692 // Reload the page with the keys.
17693 $next_page = $this->is_anonymous() ?
17694 // If user previously skipped, redirect to account page.
17695 $this->get_account_url( false, $extra ) :
17696 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17697 }
17698
17699 if ( ! empty( $next_page ) && $redirect ) {
17700 fs_redirect( $next_page );
17701 }
17702
17703 return $next_page;
17704 }
17705
17706 /**
17707 * Install plugin with new user information after approval.
17708 *
17709 * @author Vova Feldman (@svovaf)
17710 * @since 1.0.7
17711 */
17712 function _install_with_new_user() {
17713 $this->_logger->entrance();
17714
17715 if ( $this->is_registered() ) {
17716 return;
17717 }
17718
17719 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17720
17721 $this->update_license_required_permissions_if_anonymous();
17722
17723 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17724 // @todo This logic should be improved because it's executed on every load of a theme.
17725 $this->is_theme()
17726 ) {
17727 // check_admin_referer( $this->_slug . '_activate_new' );
17728
17729 if ( fs_request_has( 'user_secret_key' ) ) {
17730 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17731 $pending_sites_info = $this->_storage->pending_sites_info;
17732
17733 $this->install_many_pending_with_user(
17734 fs_request_get( 'user_id' ),
17735 fs_request_get_raw( 'user_public_key' ),
17736 fs_request_get_raw( 'user_secret_key' ),
17737 fs_request_get_bool( 'is_marketing_allowed', null ),
17738 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17739 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17740 $pending_sites_info['blog_ids'],
17741 $pending_sites_info['license_key'],
17742 $pending_sites_info['trial_plan_id']
17743 );
17744 } else {
17745 $this->install_with_new_user(
17746 fs_request_get( 'user_id' ),
17747 fs_request_get_raw( 'user_public_key' ),
17748 fs_request_get_raw( 'user_secret_key' ),
17749 fs_request_get_bool( 'is_marketing_allowed', null ),
17750 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17751 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17752 fs_request_get( 'install_id' ),
17753 fs_request_get_raw( 'install_public_key' ),
17754 fs_request_get_raw( 'install_secret_key' ),
17755 true,
17756 fs_request_get_bool( 'auto_install' )
17757 );
17758 }
17759 } else if ( $has_pending_activation_confirmation_param ) {
17760 $this->set_pending_confirmation(
17761 fs_request_get( 'user_email' ),
17762 true,
17763 false,
17764 false,
17765 fs_request_get_bool( 'is_suspicious_email' ),
17766 fs_request_get_bool( 'has_upgrade_context' ),
17767 fs_request_get( 'support_email_address' )
17768 );
17769 }
17770 }
17771 }
17772
17773 /**
17774 * @author Vova Feldman (@svovaf)
17775 * @since 2.0.0
17776 *
17777 * @param number $id
17778 * @param string $public_key
17779 * @param string $secret_key
17780 *
17781 * @return \FS_User
17782 */
17783 private function setup_user( $id, $public_key, $secret_key ) {
17784 $user = self::_get_user_by_id( $id );
17785
17786 if ( is_object( $user ) ) {
17787 $this->_user = $user;
17788 } else {
17789 $user = new FS_User();
17790 $user->id = $id;
17791 $user->public_key = $public_key;
17792 $user->secret_key = $secret_key;
17793
17794 $this->_user = $user;
17795 $user_result = $this->get_api_user_scope()->get();
17796 $user = new FS_User( $user_result );
17797
17798 $this->_user = $user;
17799 $this->_store_user();
17800 }
17801
17802 return $user;
17803 }
17804
17805 /**
17806 * Install plugin with new user.
17807 *
17808 * @author Vova Feldman (@svovaf)
17809 * @since 1.1.7.4
17810 *
17811 * @param number $user_id
17812 * @param string $user_public_key
17813 * @param string $user_secret_key
17814 * @param bool|null $is_marketing_allowed
17815 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17816 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17817 * @param number $install_id
17818 * @param string $install_public_key
17819 * @param string $install_secret_key
17820 * @param bool $redirect
17821 * @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.
17822 *
17823 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17824 */
17825 private function install_with_new_user(
17826 $user_id,
17827 $user_public_key,
17828 $user_secret_key,
17829 $is_marketing_allowed,
17830 $is_extensions_tracking_allowed,
17831 $is_diagnostic_tracking_allowed,
17832 $install_id,
17833 $install_public_key,
17834 $install_secret_key,
17835 $redirect = true,
17836 $auto_install = false
17837 ) {
17838 /**
17839 * This method is also executed after opting in with a license key since the
17840 * license can be potentially associated with a different owner.
17841 *
17842 * @since 2.0.0
17843 */
17844 $user = self::_get_user_by_id( $user_id );
17845
17846 if ( ! is_object( $user ) ) {
17847 $user = new FS_User();
17848 $user->id = $user_id;
17849 $user->public_key = $user_public_key;
17850 $user->secret_key = $user_secret_key;
17851
17852 $this->_user = $user;
17853 $user_result = $this->get_api_user_scope()->get();
17854 $user = new FS_User( $user_result );
17855 }
17856
17857 $this->_user = $user;
17858
17859 $site = new FS_Site();
17860 $site->id = $install_id;
17861 $site->public_key = $install_public_key;
17862 $site->secret_key = $install_secret_key;
17863
17864 $this->_site = $site;
17865 $site_result = $this->get_api_site_scope( true )->get();
17866 $site = new FS_Site( $site_result );
17867 $this->_site = $site;
17868
17869 if ( ! is_null( $is_marketing_allowed ) ) {
17870 $this->disable_opt_in_notice_and_lock_user();
17871 }
17872
17873 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17874 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17875 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17876 ) );
17877
17878 return $this->setup_account(
17879 $this->_user,
17880 $this->_site,
17881 $redirect,
17882 $auto_install
17883 );
17884 }
17885
17886 /**
17887 * Install plugin with user.
17888 *
17889 * @author Leo Fajardo (@leorw)
17890 * @since 2.0.0
17891 *
17892 * @param number $user_id
17893 * @param string $user_public_key
17894 * @param string $user_secret_key
17895 * @param bool|null $is_marketing_allowed
17896 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17897 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17898 * @param array $site_ids
17899 * @param bool $license_key
17900 * @param bool $trial_plan_id
17901 * @param bool $redirect
17902 *
17903 * @return void
17904 */
17905 private function install_many_pending_with_user(
17906 $user_id,
17907 $user_public_key,
17908 $user_secret_key,
17909 $is_marketing_allowed,
17910 $is_extensions_tracking_allowed,
17911 $is_diagnostic_tracking_allowed,
17912 $site_ids,
17913 $license_key = false,
17914 $trial_plan_id = false,
17915 $redirect = true
17916 ) {
17917 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17918
17919 if ( ! is_null( $is_marketing_allowed ) ) {
17920 $this->disable_opt_in_notice_and_lock_user();
17921 }
17922
17923 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17924 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17925 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17926 ) );
17927
17928 $sites = array();
17929 foreach ( $site_ids as $site_id ) {
17930 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17931 }
17932
17933 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17934 }
17935
17936 /**
17937 * Multi-site install with a new user.
17938 *
17939 * @author Vova Feldman (@svovaf)
17940 * @since 2.0.0
17941 *
17942 * @param number $user_id
17943 * @param string $user_public_key
17944 * @param string $user_secret_key
17945 * @param bool|null $is_marketing_allowed
17946 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17947 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17948 * @param object[] $installs
17949 * @param bool $redirect
17950 * @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.
17951 *
17952 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17953 */
17954 private function install_many_with_new_user(
17955 $user_id,
17956 $user_public_key,
17957 $user_secret_key,
17958 $is_marketing_allowed,
17959 $is_extensions_tracking_allowed,
17960 $is_diagnostic_tracking_allowed,
17961 array $installs,
17962 $redirect = true,
17963 $auto_install = false
17964 ) {
17965 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17966
17967 if ( ! is_null( $is_marketing_allowed ) ) {
17968 $this->disable_opt_in_notice_and_lock_user();
17969 }
17970
17971 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17972 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17973 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17974 ) );
17975
17976 $install_ids = array();
17977
17978 foreach ( $installs as $install ) {
17979 $install_ids[] = $install->id;
17980 }
17981
17982 $items_per_request = 25;
17983 $left = count( $install_ids );
17984 $offset = 0;
17985
17986 $installs = array();
17987 while ( $left > 0 ) {
17988 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
17989
17990 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17991 // @todo Handle API error.
17992 }
17993
17994 $installs = array_merge( $installs, $result->installs );
17995
17996 $left -= $items_per_request;
17997 $offset += $items_per_request;
17998 }
17999
18000 foreach ( $installs as &$install ) {
18001 $install = new FS_Site( $install );
18002 }
18003
18004 return $this->setup_network_account(
18005 $this->_user,
18006 $installs,
18007 $redirect,
18008 $auto_install
18009 );
18010 }
18011
18012 /**
18013 * @author Vova Feldman (@svovaf)
18014 * @since 1.1.7.4
18015 *
18016 * @param string|bool $email
18017 * @param bool $redirect
18018 * @param string|bool $license_key Since 1.2.1.5
18019 * @param bool $is_pending_trial Since 1.2.1.5
18020 * @param bool $is_suspicious_email Since 2.5.0
18021 * @param bool $has_upgrade_context Since 2.5.3
18022 * @param bool|string $support_email_address Since 2.5.3
18023 *
18024 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
18025 */
18026 private function set_pending_confirmation(
18027 $email = false,
18028 $redirect = true,
18029 $license_key = false,
18030 $is_pending_trial = false,
18031 $is_suspicious_email = false,
18032 $has_upgrade_context = false,
18033 $support_email_address = false
18034 ) {
18035 $is_network_admin = fs_is_network_admin();
18036
18037 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
18038 /**
18039 * If explicitly asked to ignore pending mode, set to anonymous mode
18040 * 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).
18041 *
18042 * @author Vova Feldman
18043 * @since 1.2.1.6
18044 */
18045 $this->skip_connection( $is_network_admin );
18046 } else {
18047 // Install must be activated via email since
18048 // user with the same email already exist.
18049 $this->_storage->is_pending_activation = true;
18050 $this->_add_pending_activation_notice(
18051 $email,
18052 $is_pending_trial,
18053 $is_suspicious_email,
18054 $has_upgrade_context,
18055 $support_email_address
18056 );
18057 }
18058
18059 if ( ! empty( $license_key ) ) {
18060 $this->_storage->pending_license_key = $license_key;
18061 }
18062
18063 // Remove the opt-in sticky notice.
18064 $this->_admin_notices->remove_sticky( array(
18065 'connect_account',
18066 'trial_promotion',
18067 ) );
18068
18069 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
18070
18071 if ( $redirect ) {
18072 // Reload the page with a pending activation message.
18073 fs_redirect( $next_page );
18074 }
18075
18076 return $next_page;
18077 }
18078
18079 /**
18080 * Install plugin with current logged WP user info.
18081 *
18082 * @author Vova Feldman (@svovaf)
18083 * @since 1.0.7
18084 */
18085 function _install_with_current_user() {
18086 $this->_logger->entrance();
18087
18088 if ( $this->is_registered() ) {
18089 return;
18090 }
18091
18092 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
18093 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
18094
18095 /**
18096 * @author Vova Feldman (@svovaf)
18097 * @since 1.1.9 Add license key if given.
18098 */
18099 $license_key = fs_request_get_raw( 'license_secret_key' );
18100
18101 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18102 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
18103 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
18104 ) );
18105
18106 $this->install_with_current_user( $license_key );
18107 }
18108 }
18109
18110
18111 /**
18112 * @author Vova Feldman (@svovaf)
18113 * @since 1.1.7.4
18114 *
18115 * @param string|bool $license_key
18116 * @param number|bool $trial_plan_id
18117 * @param array $sites Since 2.0.0
18118 * @param bool $redirect
18119 *
18120 * @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.
18121 */
18122 function install_with_current_user(
18123 $license_key = false,
18124 $trial_plan_id = false,
18125 $sites = array(),
18126 $redirect = true
18127 ) {
18128 // Get current logged WP user.
18129 $current_user = self::_get_current_wp_user();
18130
18131 // Find the relevant FS user by the email.
18132 $user = self::_get_user_by_email( $current_user->user_email );
18133
18134 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18135 }
18136
18137 /**
18138 * @author Vova Feldman (@svovaf)
18139 * @since 2.0.0
18140 *
18141 * @param \FS_User $user
18142 * @param string|bool $license_key
18143 * @param number|bool $trial_plan_id
18144 * @param bool $redirect
18145 * @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.
18146 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18147 *
18148 * @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.
18149 */
18150 function install_with_user(
18151 FS_User $user,
18152 $license_key = false,
18153 $trial_plan_id = false,
18154 $redirect = true,
18155 $setup_account = true,
18156 $sites = array()
18157 ) {
18158 // We have to set the user before getting user scope API handler.
18159 $this->_user = $user;
18160
18161 // Install the plugin.
18162 $result = $this->create_installs_with_user(
18163 $user,
18164 $license_key,
18165 $trial_plan_id,
18166 $sites,
18167 $redirect
18168 );
18169
18170 if ( ! $this->is_api_result_entity( $result ) &&
18171 ! $this->is_api_result_object( $result, 'installs' )
18172 ) {
18173 // @todo Handler potential API error of the $result
18174 }
18175
18176 if ( empty( $sites ) ) {
18177 $site = new FS_Site( $result );
18178 $this->_site = $site;
18179
18180 if ( ! $setup_account ) {
18181 $this->_store_site();
18182
18183 $this->sync_plan_if_not_exist( $site->plan_id );
18184
18185 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18186 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18187 }
18188
18189 $this->_admin_notices->remove_sticky( 'connect_account', false );
18190
18191 return $site;
18192 }
18193
18194 return $this->setup_account( $this->_user, $this->_site, $redirect );
18195 } else {
18196 $installs = array();
18197 foreach ( $result->installs as $install ) {
18198 $installs[] = new FS_Site( $install );
18199 }
18200
18201 return $this->setup_network_account(
18202 $user,
18203 $installs,
18204 $redirect
18205 );
18206 }
18207 }
18208
18209 /**
18210 * Initiate an API request to create a collection of installs.
18211 *
18212 * @author Vova Feldman (@svovaf)
18213 * @since 2.0.0
18214 *
18215 * @param \FS_User $user
18216 * @param bool $license_key
18217 * @param bool $trial_plan_id
18218 * @param array $sites
18219 * @param bool $redirect
18220 * @param bool $silent
18221 *
18222 * @return object|mixed
18223 */
18224 private function create_installs_with_user(
18225 FS_User $user,
18226 $license_key = false,
18227 $trial_plan_id = false,
18228 $sites = array(),
18229 $redirect = false,
18230 $silent = false
18231 ) {
18232 $extra_install_params = array(
18233 'uid' => $this->get_anonymous_id(),
18234 'is_disconnected' => false,
18235 );
18236
18237 if ( ! empty( $license_key ) ) {
18238 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18239
18240 if ( $silent ) {
18241 $extra_install_params['ignore_license_owner'] = true;
18242 }
18243 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18244 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18245 }
18246
18247 if ( ! empty( $sites ) ) {
18248 $extra_install_params['sites'] = $sites;
18249 }
18250
18251 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18252
18253 // Install the plugin.
18254 $result = $this->get_api_user_scope_by_user( $user )->call(
18255 "/plugins/{$this->get_id()}/installs.json",
18256 'post',
18257 $args
18258 );
18259
18260 if ( ! $this->is_api_result_entity( $result ) &&
18261 ! $this->is_api_result_object( $result, 'installs' )
18262 ) {
18263 if ( ! empty( $args['license_key'] ) ) {
18264 // Pass the fully entered license key to the failure handler.
18265 $args['license_key'] = $license_key;
18266 }
18267
18268 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18269
18270 if ( ! $silent ) {
18271 $this->_admin_notices->add(
18272 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18273 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18274 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18275 'error'
18276 );
18277 }
18278
18279 if ( $redirect ) {
18280 /**
18281 * We set the user before getting the user scope API handler, so the user became temporarily
18282 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18283 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18284 * activation page based on the return value of `is_registered()`. In addition, in case the
18285 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18286 * misleading plugin activation errors will be shown on the `Plugins` page.
18287 *
18288 * @author Leo Fajardo (@leorw)
18289 */
18290 $this->_user = null;
18291
18292 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18293 }
18294 }
18295
18296 return $result;
18297 }
18298
18299 /**
18300 * Tries to activate add-on account based on parent plugin info.
18301 *
18302 * @author Vova Feldman (@svovaf)
18303 * @since 1.0.6
18304 *
18305 * @param Freemius $parent_fs
18306 * @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.
18307 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18308 */
18309 private function _activate_addon_account(
18310 Freemius $parent_fs,
18311 $network_level_or_blog_id = null,
18312 FS_Plugin_License $bundle_license = null
18313 ) {
18314 if ( $this->is_registered() ) {
18315 // Already activated.
18316 return;
18317 }
18318
18319 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18320 $permissions = array();
18321 foreach ( $permission_ids as $permission_id ) {
18322 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18323 }
18324
18325 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18326
18327 /**
18328 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18329 * already returns the data for the current blog.
18330 *
18331 * @author Leo Fajardo (@leorw)
18332 * @since 2.3.0
18333 */
18334 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18335 array() :
18336 array( 'uid' => $this->get_anonymous_id() );
18337
18338 $params = $this->get_install_data_for_api(
18339 $uid_param_to_override,
18340 false,
18341 false,
18342 /**
18343 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18344 * already includes the data for it.
18345 *
18346 * @author Leo Fajardo (@leorw)
18347 * @since 2.3.0
18348 */
18349 ( true !== $network_level_or_blog_id )
18350 );
18351
18352 if ( true === $network_level_or_blog_id ) {
18353 $params['sites'] = $this->get_sites_for_network_level_optin();
18354
18355 if ( empty( $params['sites'] ) ) {
18356 return;
18357 }
18358 }
18359
18360 if ( is_object( $bundle_license ) ) {
18361 $params['license_key'] = $bundle_license->secret_key;
18362 }
18363
18364 // Activate add-on with parent plugin credentials.
18365 $result = $parent_fs->get_api_site_scope()->call(
18366 "/addons/{$this->_plugin->id}/installs.json",
18367 'post',
18368 $params
18369 );
18370
18371 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18372 if ( is_object( $bundle_license ) ) {
18373 /**
18374 * 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.
18375 *
18376 * @author Leo Fajardo (@leorw)
18377 * @since 2.4.0
18378 */
18379 } else {
18380 $error_message = FS_Api::is_api_error_object( $result ) ?
18381 $result->error->message :
18382 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18383
18384 $this->_admin_notices->add(
18385 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18386 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18387 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18388 'error'
18389 );
18390 }
18391
18392 return;
18393 }
18394
18395 $addon_installs = $result->installs;
18396 foreach ( $addon_installs as $key => $addon_install ) {
18397 $addon_installs[ $key ] = new FS_Site( $addon_install );
18398 }
18399
18400 $first_install = $addon_installs[0];
18401
18402 // Get user information based on parent's plugin.
18403 $user = $parent_fs->get_user();
18404
18405 // First of all, set site and user info - otherwise we won't
18406 // be able to invoke API calls.
18407 $this->_site = $first_install;
18408 $this->_user = $user;
18409
18410 // Sync add-on plans.
18411 $this->_sync_plans();
18412
18413 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18414
18415 // Get site's current plan.
18416 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18417
18418 // Sync licenses.
18419 $this->_sync_licenses();
18420
18421 if ( ! fs_is_network_admin() ) {
18422 // Try to activate premium license.
18423 $this->_activate_license( true, $bundle_license );
18424
18425 if ( is_object( $bundle_license ) ) {
18426 $this->maybe_activate_bundle_license( $bundle_license );
18427 }
18428 } else {
18429 if ( is_object( $bundle_license ) ) {
18430 $premium_license = $bundle_license;
18431 } else {
18432 $license_id = fs_request_get( 'license_id' );
18433
18434 if ( is_object( $this->_site ) &&
18435 FS_Plugin_License::is_valid_id( $license_id ) &&
18436 $license_id == $this->_site->license_id
18437 ) {
18438 // License is already activated.
18439 return;
18440 }
18441
18442 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18443 $this->_get_license_by_id( $license_id ) :
18444 $this->_get_available_premium_license();
18445 }
18446
18447 if ( is_object( $premium_license ) ) {
18448 $this->maybe_network_activate_addon_license( $premium_license );
18449 }
18450 }
18451 }
18452
18453 /**
18454 * @author Leo Fajardo (@leorw)
18455 * @since 2.3.0
18456 *
18457 * @param FS_Site[] $installs
18458 * @param bool $is_site_level
18459 */
18460 private function handle_account_connection( $installs, $is_site_level ) {
18461 $first_install = $installs[0];
18462
18463 if ( $is_site_level ) {
18464 $this->_set_account( $this->_user, $first_install );
18465
18466 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18467 } else {
18468 $this->_store_user();
18469
18470 // Map site addresses to their blog IDs.
18471 $address_to_blog_map = $this->get_address_to_blog_map();
18472
18473 $first_blog_id = null;
18474 $blog_2_install_map = array();
18475 foreach ( $installs as $install ) {
18476 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18477 $blog_id = $address_to_blog_map[ $address ];
18478
18479 $this->_store_site( true, $blog_id, $install );
18480
18481 if ( is_null( $first_blog_id ) ) {
18482 $first_blog_id = $blog_id;
18483 }
18484
18485 $blog_2_install_map[ $blog_id ] = $install;
18486 }
18487
18488 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18489 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18490 ) {
18491 // Store network user.
18492 $this->_storage->network_user_id = $this->_user->id;
18493 }
18494
18495 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18496 $this->_storage->network_install_blog_id = $first_blog_id;
18497 }
18498
18499 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18500 // Super admin opted in for all sites in the network.
18501 $this->_storage->is_network_connected = true;
18502 }
18503
18504 $this->_store_licenses( false );
18505
18506 self::$_accounts->store();
18507
18508 // Don't sync the installs data on network upgrade
18509 if ( ! $this->network_upgrade_mode_completed() ) {
18510 $this->send_installs_update();
18511 }
18512
18513 $current_blog = get_current_blog_id();
18514
18515 foreach ( $blog_2_install_map as $blog_id => $install ) {
18516 $this->switch_to_blog( $blog_id );
18517
18518 $this->do_action( 'after_account_connection', $this->_user, $install );
18519 }
18520
18521 // Switch install context back to the first install.
18522 $this->switch_to_blog(
18523 $current_blog,
18524 $first_install,
18525 ( $this->_site->id != $first_install->id )
18526 );
18527
18528 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18529 }
18530 }
18531
18532 /**
18533 * Tries to activate parent account based on add-on's info.
18534 *
18535 * @author Vova Feldman (@svovaf)
18536 * @since 1.2.2.7
18537 *
18538 * @param Freemius $parent_fs
18539 */
18540 private function activate_parent_account( Freemius $parent_fs ) {
18541 if ( ! $this->is_addon() ) {
18542 // This is not an add-on.
18543 return;
18544 }
18545
18546 if ( $parent_fs->is_registered() ) {
18547 // Already activated.
18548 return;
18549 }
18550
18551 // Activate parent with add-on's user credentials.
18552 $parent_install = $this->get_api_user_scope()->call(
18553 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18554 'post',
18555 $parent_fs->get_install_data_for_api( array(
18556 'uid' => $parent_fs->get_anonymous_id(),
18557 ), false, false )
18558 );
18559
18560 if ( isset( $parent_install->error ) ) {
18561 $this->_admin_notices->add(
18562 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18563 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18564 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18565 'error'
18566 );
18567
18568 return;
18569 }
18570
18571 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18572
18573 if ( $parent_fs->is_pending_activation() ) {
18574 $parent_fs->clear_pending_activation_mode();
18575 }
18576
18577 // Get user information based on parent's plugin.
18578 $user = $this->get_user();
18579
18580 // First of all, set site info - otherwise we won't
18581 // be able to invoke API calls.
18582 $parent_fs->_site = new FS_Site( $parent_install );
18583 $parent_fs->_user = $user;
18584
18585 // Sync add-on plans.
18586 $parent_fs->_sync_plans();
18587
18588 $parent_fs->update_license_required_permissions_if_anonymous();
18589
18590 $parent_fs->_set_account( $user, $parent_fs->_site );
18591 }
18592
18593 #endregion
18594
18595 #----------------------------------------------------------------------------------
18596 #region Admin Menu Items
18597 #----------------------------------------------------------------------------------
18598
18599 private $_menu_items = array();
18600
18601 /**
18602 * @author Vova Feldman (@svovaf)
18603 * @since 1.2.1.8
18604 *
18605 * @return array
18606 */
18607 function get_menu_items() {
18608 return $this->_menu_items;
18609 }
18610
18611 /**
18612 * @author Vova Feldman (@svovaf)
18613 * @since 1.0.7
18614 *
18615 * @return string
18616 */
18617 function get_menu_slug() {
18618 return $this->_menu->get_slug();
18619 }
18620
18621 /**
18622 * @author Vova Feldman (@svovaf)
18623 * @since 1.0.9
18624 */
18625 function _prepare_admin_menu() {
18626 // if ( ! $this->is_on() ) {
18627 // return;
18628 // }
18629
18630 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18631 return;
18632 }
18633
18634 /**
18635 * When running from a site admin with a network activated module and the connection
18636 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18637 * site level settings.
18638 *
18639 * @author Vova Feldman (@svovaf)
18640 * @since 2.0.0
18641 */
18642 $should_hide_site_admin_settings = (
18643 $this->_is_network_active &&
18644 ! fs_is_network_admin() &&
18645 ! $this->is_delegated_connection() &&
18646 ! $this->is_anonymous() &&
18647 ! $this->is_registered()
18648 );
18649
18650 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18651
18652 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18653 $should_hide_site_admin_settings
18654 ) {
18655 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18656 } else {
18657 $this->do_action( fs_is_network_admin() ?
18658 'before_network_admin_menu_init' :
18659 'before_admin_menu_init'
18660 );
18661
18662 $this->add_menu_action();
18663
18664 $this->add_network_menu_when_missing();
18665
18666 $this->add_submenu_items();
18667 }
18668 }
18669
18670 /**
18671 * Admin dashboard menu items modifications.
18672 *
18673 * NOTE: admin_menu action executed before admin_init.
18674 *
18675 * @author Vova Feldman (@svovaf)
18676 * @since 1.0.7
18677 *
18678 */
18679 private function add_menu_action() {
18680 if ( $this->is_activation_mode() ) {
18681 if ( $this->show_opt_in_on_setting_page() ) {
18682 $this->override_plugin_menu_with_activation();
18683 } else {
18684 /**
18685 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18686 */
18687 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18688 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18689 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18690 fs_request_get_bool( 'pending_activation' )
18691 ) {
18692 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18693 }
18694 }
18695 } else {
18696 if ( ! $this->is_registered() ) {
18697 // If not registered try to install user.
18698 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18699 $this->_install_with_new_user();
18700 }
18701 } else if (
18702 fs_request_is_action( 'sync_user' ) &&
18703 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18704 ) {
18705 $this->_handle_account_user_sync();
18706 }
18707 }
18708 }
18709
18710 /**
18711 * @author Vova Feldman (@svovaf)
18712 * @since 1.0.1
18713 */
18714 function _redirect_on_clicked_menu_link() {
18715 $this->_logger->entrance();
18716
18717 $page = fs_request_get('page');
18718 $page = is_string($page) ? strtolower($page) : '';
18719
18720 $this->_logger->log( 'page = ' . $page );
18721
18722 foreach ( $this->_menu_items as $priority => $items ) {
18723 foreach ( $items as $item ) {
18724 if ( isset( $item['url'] ) ) {
18725 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18726 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18727
18728 fs_redirect( $item['url'] );
18729 }
18730 }
18731 }
18732 }
18733 }
18734
18735 /**
18736 * Remove plugin's all admin menu items & pages, and replace with activation page.
18737 *
18738 * @author Vova Feldman (@svovaf)
18739 * @since 1.0.1
18740 */
18741 private function override_plugin_menu_with_activation() {
18742 $this->_logger->entrance();
18743
18744 $hook = false;
18745
18746 if ( ! $this->has_settings_menu() ) {
18747 // Add the opt-in page without a menu item.
18748 $hook = FS_Admin_Menu_Manager::add_subpage(
18749 '',
18750 $this->get_plugin_name(),
18751 $this->get_plugin_name(),
18752 'manage_options',
18753 $this->_slug,
18754 array( &$this, '_connect_page_render' )
18755 );
18756 } else if ( $this->_menu->is_top_level() ) {
18757 if ( $this->_menu->is_override_exact() ) {
18758 // Make sure the current page is matching the activation page.
18759 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18760 return;
18761 }
18762 }
18763
18764 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18765
18766 if ( false === $hook ) {
18767 // Create new menu item just for the opt-in.
18768 $hook = FS_Admin_Menu_Manager::add_page(
18769 $this->get_plugin_name(),
18770 $this->get_plugin_name(),
18771 'manage_options',
18772 $this->_menu->get_slug(),
18773 array( &$this, '_connect_page_render' )
18774 );
18775 }
18776 } else {
18777 $menus = array( $this->_menu->get_parent_slug() );
18778
18779 if ( $this->_menu->is_override_exact() ) {
18780 // Make sure the current page is matching the activation page.
18781 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18782 return;
18783 }
18784 }
18785
18786 foreach ( $menus as $parent_slug ) {
18787 $hook = $this->_menu->override_submenu_action(
18788 $parent_slug,
18789 $this->_menu->get_raw_slug(),
18790 array( &$this, '_connect_page_render' )
18791 );
18792
18793 if ( false !== $hook ) {
18794 // Found plugin's submenu item.
18795 break;
18796 }
18797 }
18798 }
18799
18800 if ( $this->is_activation_page() ) {
18801 // Clean admin page from distracting content.
18802 self::_clean_admin_content_section();
18803 }
18804
18805 if ( false !== $hook ) {
18806 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18807 $this->_install_with_current_user();
18808 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18809 $this->_install_with_new_user();
18810 }
18811 }
18812 }
18813
18814 /**
18815 * If a plugin was network activated and connected but don't have a network
18816 * level settings, then add an artificial menu item for the Account and other
18817 * Freemius settings.
18818 *
18819 * @author Vova Feldman (@svovaf)
18820 * @since 2.0.0
18821 */
18822 private function add_network_menu_when_missing() {
18823 $this->_logger->entrance();
18824
18825 if ( ! $this->_is_network_active ) {
18826 // Plugin wasn't activated on the network level.
18827 return;
18828 }
18829
18830 if ( ! fs_is_network_admin() ) {
18831 // The context is not the network admin.
18832 return;
18833 }
18834
18835 if ( $this->_menu->has_network_menu() ) {
18836 // Plugin already has a network level menu.
18837 return;
18838 }
18839
18840 if ( $this->is_network_activation_mode() ) {
18841 /**
18842 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18843 * screen is being shown.
18844 *
18845 * @author Leo Fajardo (@leorw)
18846 */
18847 return;
18848 }
18849
18850 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18851 if ( $this->is_network_delegated_connection() ) {
18852 // Super-admin delegated the connection to the site admins.
18853 return;
18854 }
18855 }
18856
18857 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18858
18859 if ( $this->_menu->has_menu() ||
18860 ! $this->is_addon() ||
18861 $this->is_activation_mode()
18862 ) {
18863 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18864 $this->get_plugin_name(),
18865 $this->get_plugin_name(),
18866 'manage_options',
18867 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18868 );
18869 }
18870 } else {
18871 $this->_menu->add_subpage_and_update(
18872 $this->_menu->get_parent_slug(),
18873 $this->get_plugin_name(),
18874 $this->get_plugin_name(),
18875 'manage_options',
18876 $this->_menu->get_slug()
18877 );
18878 }
18879 }
18880
18881 /**
18882 * @author Leo Fajardo (@leorw)
18883 * @since 1.2.1
18884 *
18885 * return string
18886 */
18887 function get_top_level_menu_capability() {
18888 global $menu;
18889
18890 $top_level_menu_slug = $this->get_top_level_menu_slug();
18891
18892 foreach ( $menu as $menu_info ) {
18893 /**
18894 * The second element in the menu info array is the capability/role that has access to the menu and the
18895 * third element is the menu slug.
18896 */
18897 if ( $menu_info[2] === $top_level_menu_slug ) {
18898 return $menu_info[1];
18899 }
18900 }
18901
18902 return 'read';
18903 }
18904
18905 /**
18906 * @author Vova Feldman (@svovaf)
18907 * @since 1.0.0
18908 *
18909 * @return string
18910 */
18911 private function get_top_level_menu_slug() {
18912 return ( $this->is_addon() ?
18913 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18914 $this->_menu->get_top_level_menu_slug() );
18915 }
18916
18917 /**
18918 * @author Vova Feldman (@svovaf)
18919 * @since 1.2.2.7
18920 *
18921 * @return string
18922 */
18923 function get_pricing_cta_label() {
18924 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18925
18926 if ( $this->is_in_trial_promotion() &&
18927 ! $this->is_paying_or_trial()
18928 ) {
18929 // If running a trial promotion, modify the pricing to load the trial.
18930 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18931 } else if ( $this->is_paying() ) {
18932 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18933 }
18934
18935 return $label;
18936 }
18937
18938 /**
18939 * @author Vova Feldman (@svovaf)
18940 * @since 1.2.2.7
18941 *
18942 * @return bool
18943 */
18944 function is_pricing_page_visible() {
18945 return (
18946 // Has at least one paid plan.
18947 $this->has_paid_plan() &&
18948 // Didn't ask to hide the pricing page.
18949 $this->is_page_visible( 'pricing' ) &&
18950 // Don't have a valid active license or has more than one plan.
18951 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18952 );
18953 }
18954
18955 /**
18956 * @author Leo Fajardo (@leorw)
18957 * @since 2.3.0
18958 *
18959 * @param bool $is_activation_mode
18960 *
18961 * @return bool
18962 */
18963 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18964 if ( $this->is_addon() ) {
18965 // No submenu items or action links for add-ons.
18966 return false;
18967 }
18968
18969 if ( $this->show_opt_in_on_themes_page() ) {
18970 if ( ! fs_is_network_admin() ) {
18971 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18972 return true;
18973 }
18974 } else if ( $is_activation_mode ) {
18975 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18976 return false;
18977 }
18978
18979 if ( fs_is_network_admin() ) {
18980 /**
18981 * Add submenu items or action links to network level when plugin was network activated and the super
18982 * admin did NOT delegate the connection of all sites to site admins.
18983 */
18984 return (
18985 $this->_is_network_active &&
18986 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18987 ! $this->is_network_delegated_connection() )
18988 );
18989 }
18990
18991 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18992 }
18993
18994 /**
18995 * Add default Freemius menu items.
18996 *
18997 * @author Vova Feldman (@svovaf)
18998 * @since 1.0.0
18999 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
19000 */
19001 private function add_submenu_items() {
19002 $this->_logger->entrance();
19003
19004 $is_activation_mode = $this->is_activation_mode();
19005
19006 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
19007
19008 if ( $add_submenu_items ) {
19009 if ( $this->has_affiliate_program() ) {
19010 // Add affiliation page.
19011 $this->add_submenu_item(
19012 $this->get_text_inline( 'Affiliation', 'affiliation' ),
19013 array( &$this, '_affiliation_page_render' ),
19014 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
19015 'manage_options',
19016 'affiliation',
19017 'Freemius::_clean_admin_content_section',
19018 WP_FS__DEFAULT_PRIORITY,
19019 $this->is_submenu_item_visible( 'affiliation' )
19020 );
19021 }
19022 }
19023
19024 if ( $add_submenu_items ||
19025 ( $is_activation_mode &&
19026 $this->is_only_premium() &&
19027 $this->is_admin_page( 'account' ) &&
19028 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
19029 )
19030 ) {
19031 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
19032 $show_account = (
19033 $this->is_submenu_item_visible( 'account' ) &&
19034 /**
19035 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
19036 */
19037 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
19038 );
19039
19040 // Add user account page.
19041 $this->add_submenu_item(
19042 $this->get_text_inline( 'Account', 'account' ),
19043 array( &$this, '_account_page_render' ),
19044 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
19045 'manage_options',
19046 'account',
19047 array( &$this, '_account_page_load' ),
19048 WP_FS__DEFAULT_PRIORITY,
19049 ( $add_submenu_items && $show_account )
19050 );
19051 }
19052 }
19053
19054 if ( $add_submenu_items ) {
19055 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19056 // Add contact page.
19057 $this->add_submenu_item(
19058 $this->get_text_inline( 'Contact Us', 'contact-us' ),
19059 array( &$this, '_contact_page_render' ),
19060 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
19061 'manage_options',
19062 'contact',
19063 'Freemius::_clean_admin_content_section',
19064 WP_FS__DEFAULT_PRIORITY,
19065 $this->is_submenu_item_visible( 'contact' )
19066 );
19067 }
19068
19069 if ( $this->has_addons() ) {
19070 $this->add_submenu_item(
19071 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19072 array( &$this, '_addons_page_render' ),
19073 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19074 'manage_options',
19075 'addons',
19076 array( &$this, '_addons_page_load' ),
19077 WP_FS__LOWEST_PRIORITY - 1,
19078 $this->is_submenu_item_visible( 'addons' )
19079 );
19080 }
19081 }
19082
19083 if ( $add_submenu_items ||
19084 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
19085 ) {
19086 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19087 $show_pricing = (
19088 $this->is_submenu_item_visible( 'pricing' ) &&
19089 $this->is_pricing_page_visible()
19090 );
19091
19092 $pricing_cta_text = $this->get_pricing_cta_label();
19093 $pricing_class = 'upgrade-mode';
19094 if ( $show_pricing ) {
19095 if ( $this->is_in_trial_promotion() &&
19096 ! $this->is_paying_or_trial()
19097 ) {
19098 // If running a trial promotion, modify the pricing to load the trial.
19099 $pricing_class = 'trial-mode';
19100 } else if ( $this->is_paying() ) {
19101 $pricing_class = '';
19102 }
19103 }
19104
19105 // Add upgrade/pricing page.
19106 $this->add_submenu_item(
19107 $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' ) ),
19108 array( &$this, '_pricing_page_render' ),
19109 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19110 'manage_options',
19111 'pricing',
19112 'Freemius::_clean_admin_content_section',
19113 WP_FS__LOWEST_PRIORITY,
19114 ( $add_submenu_items && $show_pricing ),
19115 $pricing_class
19116 );
19117 }
19118 }
19119
19120 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19121 /**
19122 * Add the other menu items if there are any when not in activation mode or license activation is not
19123 * required (license activation is required for registered or anonymous users after activating the
19124 * premium version when the site is not in trial mode or there's no active valid license).
19125 *
19126 * @author Leo Fajardo (@leorw)
19127 * @since 2.2.1
19128 */
19129 if ( 0 < count( $this->_menu_items ) ) {
19130 if ( ! $this->_menu->is_top_level() ) {
19131 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19132
19133 // Append submenu items right after the plugin's submenu item.
19134 $this->order_sub_submenu_items();
19135 } else {
19136 // Append submenu items.
19137 $this->embed_submenu_items();
19138 }
19139 }
19140 }
19141 }
19142
19143 /**
19144 * Moved the actual submenu item additions to a separated function,
19145 * in order to support sub-submenu items when the plugin's settings
19146 * only have a submenu and not top-level menu item.
19147 *
19148 * @author Vova Feldman (@svovaf)
19149 * @since 1.1.4
19150 */
19151 private function embed_submenu_items() {
19152 $item_template = $this->_menu->is_top_level() ?
19153 '<span class="fs-submenu-item %s %s %s">%s</span>' :
19154 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
19155
19156 $top_level_menu_capability = $this->get_top_level_menu_capability();
19157
19158 ksort( $this->_menu_items );
19159
19160 $is_first_submenu_item = true;
19161
19162 foreach ( $this->_menu_items as $priority => $items ) {
19163 foreach ( $items as $item ) {
19164 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19165
19166 $menu_item = sprintf(
19167 $item_template,
19168 $this->get_unique_affix(),
19169 $item['menu_slug'],
19170 ! empty( $item['class'] ) ? $item['class'] : '',
19171 $item['menu_title']
19172 );
19173
19174 $top_level_menu_slug = $this->get_top_level_menu_slug();
19175 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19176
19177 if ( ! isset( $item['url'] ) ) {
19178 $hook = FS_Admin_Menu_Manager::add_subpage(
19179 $item['show_submenu'] ?
19180 $top_level_menu_slug :
19181 '',
19182 $item['page_title'],
19183 $menu_item,
19184 $capability,
19185 $menu_slug,
19186 $item['render_function']
19187 );
19188
19189 if ( false !== $item['before_render_function'] ) {
19190 add_action( "load-$hook", $item['before_render_function'] );
19191 }
19192 } else {
19193 FS_Admin_Menu_Manager::add_subpage(
19194 $item['show_submenu'] ?
19195 $top_level_menu_slug :
19196 '',
19197 $item['page_title'],
19198 $menu_item,
19199 $capability,
19200 $menu_slug,
19201 array( $this, '' )
19202 );
19203 }
19204
19205 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19206 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19207 /**
19208 * If the top-level menu has been dynamically created, remove the first submenu item that
19209 * WordPress automatically creates when there's no submenu item whose slug matches the
19210 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19211 *
19212 * Awesome Plugin
19213 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19214 *
19215 * @author Leo Fajardo (@leorw)
19216 */
19217 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19218 }
19219
19220 $is_first_submenu_item = false;
19221 }
19222 }
19223 }
19224 }
19225
19226 /**
19227 * Re-order the submenu items so all Freemius added new submenu items
19228 * are added right after the plugin's settings submenu item.
19229 *
19230 * @author Vova Feldman (@svovaf)
19231 * @since 1.1.4
19232 */
19233 private function order_sub_submenu_items() {
19234 global $submenu;
19235
19236 $menu_slug = $this->_menu->get_top_level_menu_slug();
19237
19238 /**
19239 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19240 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19241 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19242 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19243 *
19244 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19245 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19246 * capability needed to access the parent menu as the capability for the submenus that we will add.
19247 */
19248 if ( empty( $submenu[ $menu_slug ] ) ) {
19249 return;
19250 }
19251
19252 $top_level_menu = &$submenu[ $menu_slug ];
19253
19254 $all_submenu_items_after = array();
19255
19256 $found_submenu_item = false;
19257
19258 foreach ( $top_level_menu as $submenu_id => $meta ) {
19259 if ( $found_submenu_item ) {
19260 // Remove all submenu items after the plugin's submenu item.
19261 $all_submenu_items_after[] = $meta;
19262 unset( $top_level_menu[ $submenu_id ] );
19263 }
19264
19265 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19266 // Found the submenu item, put all below.
19267 $found_submenu_item = true;
19268 continue;
19269 }
19270 }
19271
19272 // Embed all plugin's new submenu items.
19273 $this->embed_submenu_items();
19274
19275 // Start with specially high number to make sure it's appended.
19276 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19277 foreach ( $all_submenu_items_after as $meta ) {
19278 $top_level_menu[ $i ] = $meta;
19279 $i ++;
19280 }
19281
19282 // Sort submenu items.
19283 ksort( $top_level_menu );
19284 }
19285
19286 /**
19287 * Helper method to return the module's support forum URL.
19288 *
19289 * @author Vova Feldman (@svovaf)
19290 * @since 1.2.2.7
19291 *
19292 * @return string
19293 */
19294 function get_support_forum_url() {
19295 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19296 }
19297
19298 /**
19299 * Displays the Support Forum link when enabled.
19300 *
19301 * Can be filtered like so:
19302 *
19303 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19304 * if ( 'support' === $menu_id ) {
19305 * return _fs->is_registered();
19306 * }
19307 * return $is_visible;
19308 * }
19309 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19310 *
19311 */
19312 function _add_default_submenu_items() {
19313 if ( ! $this->is_on() ) {
19314 return;
19315 }
19316
19317 if ( ! $this->is_activation_mode() &&
19318 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19319 ( ! $this->_is_network_active && is_admin() ) )
19320 ) {
19321 $this->add_submenu_link_item(
19322 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19323 $this->get_support_forum_url(),
19324 'wp-support-forum',
19325 null,
19326 50,
19327 $this->is_submenu_item_visible( 'support' )
19328 );
19329 }
19330 }
19331
19332 /**
19333 * @author Vova Feldman (@svovaf)
19334 * @since 1.0.1
19335 *
19336 * @param string $menu_title
19337 * @param callable $render_function
19338 * @param bool|string $page_title
19339 * @param string $capability
19340 * @param bool|string $menu_slug
19341 * @param bool|callable $before_render_function
19342 * @param int $priority
19343 * @param bool $show_submenu
19344 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19345 */
19346 function add_submenu_item(
19347 $menu_title,
19348 $render_function,
19349 $page_title = false,
19350 $capability = 'manage_options',
19351 $menu_slug = false,
19352 $before_render_function = false,
19353 $priority = WP_FS__DEFAULT_PRIORITY,
19354 $show_submenu = true,
19355 $class = ''
19356 ) {
19357 $this->_logger->entrance( 'Title = ' . $menu_title );
19358
19359 if ( $this->is_addon() ) {
19360 $parent_fs = $this->get_parent_instance();
19361
19362 if ( is_object( $parent_fs ) ) {
19363 $parent_fs->add_submenu_item(
19364 $menu_title,
19365 $render_function,
19366 $page_title,
19367 $capability,
19368 $menu_slug,
19369 $before_render_function,
19370 $priority,
19371 $show_submenu,
19372 $class
19373 );
19374
19375 return;
19376 }
19377 }
19378
19379 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19380 $this->_menu_items[ $priority ] = array();
19381 }
19382
19383 $this->_menu_items[ $priority ][] = array(
19384 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19385 'menu_title' => $menu_title,
19386 'capability' => $capability,
19387 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19388 'render_function' => $render_function,
19389 'before_render_function' => $before_render_function,
19390 'show_submenu' => $show_submenu,
19391 'class' => $class,
19392 );
19393 }
19394
19395 /**
19396 * @author Vova Feldman (@svovaf)
19397 * @since 1.0.1
19398 *
19399 * @param string $menu_title
19400 * @param string $url
19401 * @param bool $menu_slug
19402 * @param string $capability
19403 * @param int $priority
19404 * @param bool $show_submenu
19405 */
19406 function add_submenu_link_item(
19407 $menu_title,
19408 $url,
19409 $menu_slug = false,
19410 $capability = 'read',
19411 $priority = WP_FS__DEFAULT_PRIORITY,
19412 $show_submenu = true
19413 ) {
19414 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19415
19416 if ( $this->is_addon() ) {
19417 $parent_fs = $this->get_parent_instance();
19418
19419 if ( is_object( $parent_fs ) ) {
19420 $parent_fs->add_submenu_link_item(
19421 $menu_title,
19422 $url,
19423 $menu_slug,
19424 $capability,
19425 $priority,
19426 $show_submenu
19427 );
19428
19429 return;
19430 }
19431 }
19432
19433 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19434 $this->_menu_items[ $priority ] = array();
19435 }
19436
19437 $this->_menu_items[ $priority ][] = array(
19438 'menu_title' => $menu_title,
19439 'capability' => $capability,
19440 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19441 'url' => $url,
19442 'page_title' => $menu_title,
19443 'render_function' => 'fs_dummy',
19444 'before_render_function' => '',
19445 'show_submenu' => $show_submenu,
19446 );
19447 }
19448
19449 #endregion ------------------------------------------------------------------
19450
19451 #--------------------------------------------------------------------------------
19452 #region Admin Notices
19453 #--------------------------------------------------------------------------------
19454
19455 /**
19456 * @author Vova Feldman (@svovaf)
19457 * @since 2.3.1
19458 *
19459 * @param string|string[] $ids
19460 * @param int|null $network_level_or_blog_id
19461 *
19462 * @uses FS_Admin_Notices::remove_sticky()
19463 */
19464 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19465 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19466 }
19467
19468 #endregion
19469
19470 #--------------------------------------------------------------------------------
19471 #region Actions / Hooks / Filters
19472 #--------------------------------------------------------------------------------
19473
19474 /**
19475 * @author Vova Feldman (@svovaf)
19476 * @since 1.1.7
19477 *
19478 * @param string $tag
19479 *
19480 * @return string
19481 */
19482 public function get_action_tag( $tag ) {
19483 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19484 }
19485
19486 /**
19487 * @author Vova Feldman (@svovaf)
19488 * @since 1.2.1.6
19489 *
19490 * @param string $tag
19491 * @param string $slug
19492 * @param bool $is_plugin
19493 *
19494 * @return string
19495 */
19496 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19497 $action = "fs_{$tag}";
19498
19499 if ( ! empty( $slug ) ) {
19500 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19501 }
19502
19503 return $action;
19504 }
19505
19506 /**
19507 * Returns a string that can be used to generate a unique action name,
19508 * option name, HTML element ID, or HTML element class.
19509 *
19510 * @author Leo Fajardo (@leorw)
19511 * @since 1.2.2
19512 *
19513 * @return string
19514 */
19515 public function get_unique_affix() {
19516 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19517 }
19518
19519 /**
19520 * Returns a string that can be used to generate a unique action name,
19521 * option name, HTML element ID, or HTML element class.
19522 *
19523 * @author Vova Feldman (@svovaf)
19524 * @since 1.2.2.5
19525 *
19526 * @param string $slug
19527 * @param bool $is_plugin
19528 *
19529 * @return string
19530 */
19531 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19532 $affix = $slug;
19533
19534 if ( ! $is_plugin ) {
19535 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19536 }
19537
19538 return $affix;
19539 }
19540
19541 /**
19542 * @author Vova Feldman (@svovaf)
19543 * @since 1.2.1
19544 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19545 * based on the slug for backward compatibility.
19546 *
19547 * @param string $tag
19548 *
19549 * @return string
19550 */
19551 function get_ajax_action( $tag ) {
19552 return self::get_ajax_action_static( $tag, $this->_module_id );
19553 }
19554
19555 /**
19556 * @author Vova Feldman (@svovaf)
19557 * @since 1.2.1.7
19558 *
19559 * @param string $tag
19560 *
19561 * @return string
19562 */
19563 function get_ajax_security( $tag ) {
19564 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19565 }
19566
19567 /**
19568 * @author Vova Feldman (@svovaf)
19569 * @since 1.2.1.7
19570 *
19571 * @param string $tag
19572 */
19573 function check_ajax_referer( $tag ) {
19574 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19575 }
19576
19577 /**
19578 * @author Vova Feldman (@svovaf)
19579 * @since 1.2.1.6
19580 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19581 * based on the slug for backward compatibility.
19582 *
19583 * @param string $tag
19584 * @param number|null $module_id
19585 *
19586 * @return string
19587 */
19588 static function get_ajax_action_static( $tag, $module_id = null ) {
19589 $action = "fs_{$tag}";
19590
19591 if ( ! empty( $module_id ) ) {
19592 $action .= "_{$module_id}";
19593 }
19594
19595 return $action;
19596 }
19597
19598 /**
19599 * Do action, specific for the current context plugin.
19600 *
19601 * @author Vova Feldman (@svovaf)
19602 * @since 1.0.1
19603 *
19604 * @param string $tag The name of the action to be executed.
19605 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19606 * functions hooked to the action. Default empty.
19607 *
19608 * @uses do_action()
19609 */
19610 function do_action( $tag, $arg = '' ) {
19611 $args = func_get_args();
19612
19613 $this->_logger->entrance( $tag );
19614
19615 call_user_func_array( 'do_action', array_merge(
19616 array( $this->get_action_tag( $tag ) ),
19617 array_slice( $args, 1 ) )
19618 );
19619 }
19620
19621 /**
19622 * Add action, specific for the current context plugin.
19623 *
19624 * @author Vova Feldman (@svovaf)
19625 * @since 1.0.1
19626 *
19627 * @param string $tag
19628 * @param callable $function_to_add
19629 * @param int $priority
19630 * @param int $accepted_args
19631 *
19632 * @uses add_action()
19633 */
19634 function add_action(
19635 $tag,
19636 $function_to_add,
19637 $priority = WP_FS__DEFAULT_PRIORITY,
19638 $accepted_args = 1
19639 ) {
19640 $this->_logger->entrance( $tag );
19641
19642 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19643 }
19644
19645 /**
19646 * Add AJAX action, specific for the current context plugin.
19647 *
19648 * @author Vova Feldman (@svovaf)
19649 * @since 1.2.1
19650 *
19651 * @param string $tag
19652 * @param callable $function_to_add
19653 * @param int $priority
19654 *
19655 * @uses add_action()
19656 *
19657 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19658 */
19659 function add_ajax_action(
19660 $tag,
19661 $function_to_add,
19662 $priority = WP_FS__DEFAULT_PRIORITY
19663 ) {
19664 $this->_logger->entrance( $tag );
19665
19666 return self::add_ajax_action_static(
19667 $tag,
19668 $function_to_add,
19669 $priority,
19670 $this->_module_id
19671 );
19672 }
19673
19674 /**
19675 * Add AJAX action.
19676 *
19677 * @author Vova Feldman (@svovaf)
19678 * @since 1.2.1.6
19679 *
19680 * @param string $tag
19681 * @param callable $function_to_add
19682 * @param int $priority
19683 * @param number|null $module_id
19684 *
19685 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19686 * @uses add_action()
19687 *
19688 */
19689 static function add_ajax_action_static(
19690 $tag,
19691 $function_to_add,
19692 $priority = WP_FS__DEFAULT_PRIORITY,
19693 $module_id = null
19694 ) {
19695 self::$_static_logger->entrance( $tag );
19696
19697 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19698 return false;
19699 }
19700
19701 add_action(
19702 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19703 $function_to_add,
19704 $priority,
19705 0
19706 );
19707
19708 self::$_static_logger->info( "$tag AJAX callback action added." );
19709
19710 return true;
19711 }
19712
19713 /**
19714 * Send a JSON response back to an Ajax request.
19715 *
19716 * @author Vova Feldman (@svovaf)
19717 * @since 1.2.1.5
19718 *
19719 * @param mixed $response
19720 */
19721 static function shoot_ajax_response( $response ) {
19722 wp_send_json( $response );
19723 }
19724
19725 /**
19726 * Send a JSON response back to an Ajax request, indicating success.
19727 *
19728 * @author Vova Feldman (@svovaf)
19729 * @since 1.2.1.5
19730 *
19731 * @param mixed $data Data to encode as JSON, then print and exit.
19732 */
19733 static function shoot_ajax_success( $data = null ) {
19734 wp_send_json_success( $data );
19735 }
19736
19737 /**
19738 * Send a JSON response back to an Ajax request, indicating failure.
19739 *
19740 * @author Vova Feldman (@svovaf)
19741 * @since 1.2.1.5
19742 *
19743 * @param mixed $error Optional error message.
19744 */
19745 static function shoot_ajax_failure( $error = '' ) {
19746 $result = array( 'success' => false );
19747 if ( ! empty( $error ) ) {
19748 $result['error'] = $error;
19749 }
19750
19751 wp_send_json( $result );
19752 }
19753
19754 /**
19755 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19756 *
19757 * @author Vova Feldman (@svovaf)
19758 * @since 2.5.1
19759 *
19760 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19761 *
19762 * @return string
19763 */
19764 static function ajax_url( $wrap_with = "'") {
19765 if ( fs_is_network_admin() ) {
19766 $param_name = '_fs_network_admin';
19767 } else {
19768 $param_name = '_fs_blog_admin';
19769 }
19770
19771 $url = admin_url( 'admin-ajax.php', 'relative' );
19772 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19773 $url .= "{$param_name}=true";
19774
19775 return "{$wrap_with}{$url}{$wrap_with}";
19776 }
19777
19778 /**
19779 * Apply filter, specific for the current context plugin.
19780 *
19781 * @author Vova Feldman (@svovaf)
19782 * @since 1.0.9
19783 *
19784 * @param string $tag The name of the filter hook.
19785 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19786 *
19787 * @return mixed The filtered value after all hooked functions are applied to it.
19788 *
19789 * @uses apply_filters()
19790 */
19791 function apply_filters( $tag, $value ) {
19792 $args = func_get_args();
19793
19794 $this->_logger->entrance( $tag );
19795
19796 array_unshift( $args, $this->get_unique_affix() );
19797
19798 return call_user_func_array( 'fs_apply_filter', $args );
19799 }
19800
19801 /**
19802 * Add filter, specific for the current context plugin.
19803 *
19804 * @author Vova Feldman (@svovaf)
19805 * @since 1.0.9
19806 *
19807 * @param string $tag
19808 * @param callable $function_to_add
19809 * @param int $priority
19810 * @param int $accepted_args
19811 *
19812 * @uses add_filter()
19813 */
19814 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19815 $this->_logger->entrance( $tag );
19816
19817 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19818 }
19819
19820 /**
19821 * Check if has filter.
19822 *
19823 * @author Vova Feldman (@svovaf)
19824 * @since 1.1.4
19825 *
19826 * @param string $tag
19827 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19828 *
19829 * @return false|int
19830 *
19831 * @uses has_filter()
19832 */
19833 function has_filter( $tag, $function_to_check = false ) {
19834 $this->_logger->entrance( $tag );
19835
19836 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19837 }
19838
19839 #endregion
19840
19841 /**
19842 * Override default i18n text phrases.
19843 *
19844 * @author Vova Feldman (@svovaf)
19845 * @since 1.1.6
19846 *
19847 * @param string[] string $key_value
19848 *
19849 * @uses fs_override_i18n()
19850 */
19851 function override_i18n( $key_value ) {
19852 fs_override_i18n( $key_value, $this->_slug );
19853 }
19854
19855 /* Account Page
19856 ------------------------------------------------------------------------------------------------------------------*/
19857 /**
19858 * Update site information.
19859 *
19860 * @author Vova Feldman (@svovaf)
19861 * @since 1.0.1
19862 *
19863 * @param bool $store Flush to Database if true.
19864 * @param null|int $network_level_or_blog_id Since 2.0.0
19865 * @param \FS_Site $site Since 2.0.0
19866 */
19867 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
19868 $this->_logger->entrance();
19869
19870 if ( is_null( $site ) ) {
19871 $site = $this->_site;
19872 }
19873
19874 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19875 $this->_logger->error( "Empty install ID, can't store site." );
19876
19877 return;
19878 }
19879
19880 $site_clone = clone $site;
19881
19882 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19883
19884 if (
19885 ! $is_backup &&
19886 is_object( $this->_user ) && $this->_user->id != $site->user_id
19887 ) {
19888 $this->sync_user_by_current_install( $site->user_id );
19889
19890 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19891
19892 if ( empty( $prev_stored_user_id ) &&
19893 is_object($this->_user) && $this->_user->id != $site->user_id
19894 ) {
19895 /**
19896 * Store the current user ID as the previous user ID so that the previous user can be used
19897 * as the install's owner while the new owner's details are not yet available.
19898 *
19899 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19900 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19901 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19902 * into `replica`.
19903 *
19904 * @author Leo Fajardo (@leorw)
19905 */
19906 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19907 }
19908 }
19909
19910 $sites[ $this->_slug ] = $site_clone;
19911
19912 $this->set_account_option(
19913 ( $is_backup ? 'prev_' : '' ) . 'sites',
19914 $sites,
19915 $store,
19916 $network_level_or_blog_id
19917 );
19918 }
19919
19920 /**
19921 * 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).
19922 *
19923 * @author Leo Fajardo (@leorw)
19924 * @since 2.5.0
19925 */
19926 private function back_up_site() {
19927 $this->_logger->entrance();
19928
19929 $site_clone = clone $this->_site;
19930
19931 $this->_store_site( true, null, $site_clone, true );
19932 }
19933
19934 /**
19935 * Update plugin's plans information.
19936 *
19937 * @author Vova Feldman (@svovaf)
19938 * @since 1.0.2
19939 *
19940 * @param bool $store Flush to Database if true.
19941 */
19942 private function _store_plans( $store = true ) {
19943 $this->_logger->entrance();
19944
19945 $plans = self::get_all_plans( $this->_module_type );
19946
19947 // Copy plans.
19948 $encrypted_plans = array();
19949 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19950 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19951 }
19952
19953 $plans[ $this->_slug ] = $encrypted_plans;
19954
19955 $this->set_account_option( 'plans', $plans, $store );
19956 }
19957
19958 /**
19959 * Update user's plugin licenses.
19960 *
19961 * @author Vova Feldman (@svovaf)
19962 * @since 1.0.5
19963 *
19964 * @param bool $store
19965 * @param number|bool $module_id
19966 * @param FS_Plugin_License[] $licenses
19967 */
19968 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19969 $this->_logger->entrance();
19970
19971 $all_licenses = self::get_all_licenses();
19972
19973 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19974 $module_id = $this->_module_id;
19975
19976 $user_licenses = is_array( $this->_licenses ) ?
19977 $this->_licenses :
19978 array();
19979
19980 if ( empty( $user_licenses ) ) {
19981 // If the context user doesn't have any license, don't update the licenses collection.
19982 return;
19983 }
19984
19985 $new_user_licenses_map = array();
19986 foreach ( $user_licenses as $user_license ) {
19987 $new_user_licenses_map[ $user_license->id ] = $user_license;
19988 }
19989
19990 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19991
19992 // Update user licenses.
19993 $licenses_to_update_count = count( $new_user_licenses_map );
19994 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19995 if ( 0 === $licenses_to_update_count ) {
19996 break;
19997 }
19998
19999 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
20000 // Update license.
20001 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
20002 unset( $new_user_licenses_map[ $license->id ] );
20003
20004 $licenses_to_update_count --;
20005 }
20006 }
20007
20008 if ( ! empty( $new_user_licenses_map ) ) {
20009 // Add new licenses.
20010 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
20011 }
20012
20013 $licenses = $all_licenses[ $module_id ];
20014 }
20015
20016 if ( ! isset( $all_licenses[ $module_id ] ) ) {
20017 $all_licenses[ $module_id ] = array();
20018 }
20019
20020 $all_licenses[ $module_id ] = $licenses;
20021
20022 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
20023 }
20024
20025 /**
20026 * Update user information.
20027 *
20028 * @author Vova Feldman (@svovaf)
20029 * @since 1.0.1
20030 *
20031 * @param bool $store Flush to Database if true.
20032 */
20033 private function _store_user( $store = true ) {
20034 $this->_logger->entrance();
20035
20036 if ( empty( $this->_user->id ) ) {
20037 $this->_logger->error( "Empty user ID, can't store user." );
20038
20039 return;
20040 }
20041
20042 $users = self::get_all_users();
20043 $users[ $this->_user->id ] = $this->_user;
20044 self::$_accounts->set_option( 'users', $users, $store );
20045 }
20046
20047 /**
20048 * Update new updates information.
20049 *
20050 * @author Vova Feldman (@svovaf)
20051 * @since 1.0.4
20052 *
20053 * @param FS_Plugin_Tag|null $update
20054 * @param bool $store Flush to Database if true.
20055 * @param bool|number $plugin_id
20056 */
20057 private function _store_update( $update, $store = true, $plugin_id = false ) {
20058 $this->_logger->entrance();
20059
20060 if ( $update instanceof FS_Plugin_Tag ) {
20061 $update->updated = time();
20062 }
20063
20064 if ( ! is_numeric( $plugin_id ) ) {
20065 $plugin_id = $this->_plugin->id;
20066 }
20067
20068 $updates = self::get_all_updates();
20069 $updates[ $plugin_id ] = $update;
20070 self::$_accounts->set_option( 'updates', $updates, $store );
20071 }
20072
20073 /**
20074 * Update new updates information.
20075 *
20076 * @author Vova Feldman (@svovaf)
20077 * @since 1.0.6
20078 *
20079 * @param FS_Plugin[] $plugin_addons
20080 * @param bool $store Flush to Database if true.
20081 */
20082 private function _store_addons( $plugin_addons, $store = true ) {
20083 $this->_logger->entrance();
20084
20085 $addons = self::get_all_addons();
20086 $addons[ $this->_plugin->id ] = $plugin_addons;
20087 self::$_accounts->set_option( 'addons', $addons, $store );
20088 }
20089
20090 /**
20091 * Delete plugin's associated add-ons.
20092 *
20093 * @author Vova Feldman (@svovaf)
20094 * @since 1.0.8
20095 *
20096 * @param bool $store
20097 *
20098 * @return bool
20099 */
20100 private function _delete_account_addons( $store = true ) {
20101 $all_addons = self::get_all_account_addons();
20102
20103 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20104 return false;
20105 }
20106
20107 unset( $all_addons[ $this->_plugin->id ] );
20108
20109 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20110
20111 return true;
20112 }
20113
20114 /**
20115 * Update account add-ons list.
20116 *
20117 * @author Vova Feldman (@svovaf)
20118 * @since 1.0.6
20119 *
20120 * @param FS_Plugin[] $addons
20121 * @param bool $store Flush to Database if true.
20122 */
20123 private function _store_account_addons( $addons, $store = true ) {
20124 $this->_logger->entrance();
20125
20126 $all_addons = self::get_all_account_addons();
20127 $all_addons[ $this->_plugin->id ] = $addons;
20128 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20129 }
20130
20131 /**
20132 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20133 * the valid user licenses will be fetched again and the account add-ons may be updated.
20134 *
20135 * @author Leo Fajardo (@leorw)
20136 * @since 2.2.4
20137 */
20138 private function purge_valid_user_licenses_cache() {
20139 if ( ! $this->is_registered() ) {
20140 return;
20141 }
20142
20143 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20144 }
20145
20146 /**
20147 * @author Leo Fajardo (@leorw)
20148 * @since 2.3.0
20149 *
20150 * @param array $all_licenses
20151 * @param number|null $site_license_id
20152 * @param bool $include_parent_licenses
20153 *
20154 * @return array
20155 */
20156 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20157 $foreign_licenses = array(
20158 'ids' => array(),
20159 'license_keys' => array()
20160 );
20161
20162 $parent_license_ids_map = array();
20163
20164 foreach ( $all_licenses as $license ) {
20165 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20166 continue;
20167 }
20168
20169 $foreign_licenses['ids'][] = $license->id;
20170 $foreign_licenses['license_keys'][] = $license->secret_key;
20171
20172 if (
20173 $include_parent_licenses &&
20174 is_object( $this->_license ) &&
20175 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20176 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20177 ) {
20178 /**
20179 * Include the parent license's info only if it has not been included before since child licenses
20180 * can have the same parent license.
20181 */
20182 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20183 $foreign_licenses['license_keys'][] = $license->secret_key;
20184
20185 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20186 }
20187 }
20188
20189 if ( empty( $foreign_licenses['ids'] ) ) {
20190 $foreign_licenses = array();
20191 }
20192
20193 return $foreign_licenses;
20194 }
20195
20196 /**
20197 * @author Leo Fajardo (@leorw)
20198 * @since 2.3.0
20199 *
20200 * @return string
20201 */
20202 private function get_valid_user_licenses_endpoint() {
20203 $user_licenses_endpoint = '/licenses.json?type=active' .
20204 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20205
20206 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20207
20208 if ( ! empty ( $foreign_licenses ) ) {
20209 $foreign_licenses = array(
20210 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20211 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20212 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20213 );
20214
20215 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20216 }
20217
20218 return $user_licenses_endpoint;
20219 }
20220
20221 /**
20222 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20223 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20224 * method filters out non–add-on product IDs and stores the add-on IDs.
20225 *
20226 * @author Leo Fajardo (@leorw)
20227 * @since 2.2.4
20228 *
20229 * @return stdClass[] array
20230 */
20231 private function fetch_valid_user_licenses() {
20232 $this->_logger->entrance();
20233
20234 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20235
20236 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20237 ! is_array( $result->licenses )
20238 ) {
20239 return array();
20240 }
20241
20242 return $result->licenses;
20243 }
20244
20245 /**
20246 * @author Leo Fajardo (@leorw)
20247 * @since 2.2.4
20248 *
20249 * @return number[] Account add-on IDs.
20250 */
20251 function get_updated_account_addons() {
20252 $addons = $this->get_addons();
20253 if ( empty( $addons ) ) {
20254 return array();
20255 }
20256
20257 $account_addons = $this->get_account_addons();
20258 if ( ! is_array( $account_addons ) ) {
20259 $account_addons = array();
20260 }
20261
20262 $user_licenses = $this->is_registered() ?
20263 $this->fetch_valid_user_licenses() :
20264 array();
20265
20266 if ( empty( $user_licenses ) ) {
20267 return $account_addons;
20268 }
20269
20270 $addon_ids = array();
20271 foreach ( $addons as $addon ) {
20272 $addon_ids[] = $addon->id;
20273 }
20274
20275 $license_product_ids = array();
20276
20277 foreach ( $user_licenses as $license ) {
20278 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20279 $license_product_ids = array_merge( $license_product_ids, $license->products );
20280 } else {
20281 $license_product_ids[] = $license->plugin_id;
20282 }
20283 }
20284
20285 // Filter out non–add-on IDs.
20286 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20287 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20288 $this->_store_account_addons( array_unique( $new_account_addons ) );
20289 }
20290
20291 return $new_account_addons;
20292 }
20293
20294 /**
20295 * Store account params in the Database.
20296 *
20297 * @author Vova Feldman (@svovaf)
20298 * @since 1.0.1
20299 *
20300 * @param null|int $blog_id Since 2.0.0
20301 */
20302 private function _store_account( $blog_id = null ) {
20303 $this->_logger->entrance();
20304
20305 $this->_store_site( false, $blog_id );
20306 $this->_store_user( false );
20307 $this->_store_plans( false );
20308 $this->_store_licenses( false );
20309
20310 self::$_accounts->store( $blog_id );
20311 }
20312
20313 /**
20314 * Sync user's information.
20315 *
20316 * @author Vova Feldman (@svovaf)
20317 * @since 1.0.3
20318 * @uses FS_Api
20319 */
20320 private function _handle_account_user_sync() {
20321 $this->_logger->entrance();
20322
20323 $api = $this->get_api_user_scope();
20324
20325 // Get user's information.
20326 $user = $api->get( '/', true );
20327
20328 if ( isset( $user->id ) ) {
20329 $this->_user->first = $user->first;
20330 $this->_user->last = $user->last;
20331 $this->_user->email = $user->email;
20332
20333 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20334
20335 if ( $user->is_verified &&
20336 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20337 ) {
20338 $this->_user->is_verified = true;
20339
20340 $this->do_action( 'account_email_verified', $user->email );
20341
20342 $this->_admin_notices->add(
20343 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20344 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20345 'success',
20346 // Make admin sticky if account menu item is invisible,
20347 // since the page will be auto redirected to the plugin's
20348 // main settings page, and the non-sticky message
20349 // will disappear.
20350 ! $is_menu_item_account_visible,
20351 'email_verified'
20352 );
20353 }
20354
20355 // Flush user details to DB.
20356 $this->_store_user();
20357
20358 $this->do_action( 'after_account_user_sync', $user );
20359
20360 /**
20361 * If account menu item is hidden, redirect to plugin's main settings page.
20362 *
20363 * @author Vova Feldman (@svovaf)
20364 * @since 1.1.6
20365 *
20366 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20367 */
20368 if ( ! $is_menu_item_account_visible ) {
20369 fs_redirect( $this->_get_admin_page_url() );
20370 }
20371 }
20372 }
20373
20374 /**
20375 * @author Vova Feldman (@svovaf)
20376 * @since 1.0.9
20377 * @uses FS_Api
20378 *
20379 * @param number|bool $license_id
20380 *
20381 * @return FS_Subscription|object|bool
20382 */
20383 private function _fetch_site_license_subscription( $license_id = false ) {
20384 $this->_logger->entrance();
20385 $api = $this->get_api_site_scope();
20386
20387 if ( ! is_numeric( $license_id ) ) {
20388 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20389 $this->_license->parent_license_id :
20390 $this->_license->id;
20391 }
20392
20393 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20394
20395 return ! isset( $result->error ) ?
20396 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20397 new FS_Subscription( $result->subscriptions[0] ) :
20398 false
20399 ) :
20400 $result;
20401 }
20402
20403 /**
20404 * @author Vova Feldman (@svovaf)
20405 * @since 1.0.4
20406 * @uses FS_Api
20407 *
20408 * @param number|bool $plan_id
20409 *
20410 * @return FS_Plugin_Plan|object
20411 */
20412 private function _fetch_site_plan( $plan_id = false ) {
20413 $this->_logger->entrance();
20414 $api = $this->get_api_site_scope();
20415
20416 if ( ! is_numeric( $plan_id ) ) {
20417 $plan_id = $this->_site->plan_id;
20418 }
20419
20420 $plan = $api->get( "/plans/{$plan_id}.json", true );
20421
20422 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20423 }
20424
20425 /**
20426 * @author Vova Feldman (@svovaf)
20427 * @since 1.0.5
20428 * @uses FS_Api
20429 *
20430 * @return FS_Plugin_Plan[]|object
20431 */
20432 private function _fetch_plugin_plans() {
20433 $this->_logger->entrance();
20434 $api = $this->get_current_or_network_user_api_scope();
20435
20436 /**
20437 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20438 */
20439 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20440
20441 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20442 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20443 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20444 }
20445
20446 $result = $result->plans;
20447 }
20448
20449 return $result;
20450 }
20451
20452 /**
20453 * @author Vova Feldman (@svovaf)
20454 * @since 2.0.0
20455 *
20456 * @param number $plan_id
20457 *
20458 * @return \FS_Plugin_Plan|object
20459 */
20460 private function fetch_plan_by_id( $plan_id ) {
20461 $this->_logger->entrance();
20462 $api = $this->get_current_or_network_user_api_scope();
20463
20464 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20465
20466 return $this->is_api_result_entity( $result ) ?
20467 new FS_Plugin_Plan( $result ) :
20468 $result;
20469 }
20470
20471 /**
20472 * @author Vova Feldman (@svovaf)
20473 * @since 1.0.5
20474 * @uses FS_Api
20475 *
20476 * @param number|bool $plugin_id
20477 * @param number|bool $site_license_id
20478 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20479 * @param number|null $blog_id
20480 *
20481 * @return FS_Plugin_License[]|object
20482 */
20483 private function _fetch_licenses(
20484 $plugin_id = false,
20485 $site_license_id = false,
20486 $foreign_licenses = array(),
20487 $blog_id = null
20488 ) {
20489 $this->_logger->entrance();
20490
20491 $api = $this->get_api_user_scope();
20492
20493 if ( ! is_numeric( $plugin_id ) ) {
20494 $plugin_id = $this->_plugin->id;
20495 }
20496
20497 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20498 if ( ! empty ( $foreign_licenses ) ) {
20499 $foreign_licenses = array(
20500 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20501 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20502 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20503 );
20504
20505 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20506 }
20507
20508 $result = $api->get( $user_licenses_endpoint, true );
20509
20510 $is_site_license_synced = false;
20511
20512 $api_errors = array();
20513
20514 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20515 is_array( $result->licenses )
20516 ) {
20517 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20518 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20519
20520 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20521 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20522 }
20523 }
20524
20525 $result = $result->licenses;
20526 } else {
20527 $api_errors[] = $result;
20528 $result = array();
20529 }
20530
20531 if ( ! $is_site_license_synced ) {
20532 if ( ! is_null( $blog_id ) ) {
20533 /**
20534 * If blog ID is not null, the request is for syncing of the license of a single site via the
20535 * network-level "Account" page.
20536 *
20537 * @author Leo Fajardo (@leorw)
20538 */
20539 $this->switch_to_blog( $blog_id );
20540 }
20541
20542 $api = $this->get_api_site_scope();
20543
20544 if ( is_numeric( $site_license_id ) ) {
20545 // Try to retrieve a foreign license that is linked to the install.
20546 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20547
20548 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20549 is_array( $api_result->licenses )
20550 ) {
20551 $licenses = $api_result->licenses;
20552
20553 if ( ! empty( $licenses ) ) {
20554 $result[] = new FS_Plugin_License( $licenses[0] );
20555 }
20556 } else {
20557 $api_errors[] = $api_result;
20558 }
20559 } else if (
20560 is_object( $this->_license ) &&
20561 /**
20562 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20563 * the FS instance that does the syncing is the parent FS instance.
20564 *
20565 * @author Leo Fajardo (@leorw)
20566 * @since 2.3.0
20567 */
20568 $this->_license->plugin_id == $plugin_id
20569 ) {
20570 $is_license_in_result = false;
20571 if ( ! empty( $result ) ) {
20572 foreach ( $result as $license ) {
20573 if ( $license->id == $this->_license->id ) {
20574 $is_license_in_result = true;
20575 break;
20576 }
20577 }
20578 }
20579
20580 if ( ! $is_license_in_result ) {
20581 // Fetch foreign license by ID and license key.
20582 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20583 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20584
20585 if ( $this->is_api_result_entity( $license ) ) {
20586 $result[] = new FS_Plugin_License( $license );
20587 } else {
20588 $api_errors[] = $license;
20589 }
20590 }
20591 }
20592
20593 if ( ! is_null( $blog_id ) ) {
20594 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20595 }
20596 }
20597
20598 if ( is_array( $result ) && 0 < count( $result ) ) {
20599 // If found at least one license, return license collection even if there are errors.
20600 return $result;
20601 }
20602
20603 if ( ! empty( $api_errors ) ) {
20604 // If found any errors and no licenses, return first error.
20605 return $api_errors[0];
20606 }
20607
20608 // Fallback to empty licenses list.
20609 return $result;
20610 }
20611
20612 /**
20613 * @author Vova Feldman (@svovaf)
20614 * @since 2.0.0
20615 *
20616 * @param number $license_id
20617 * @param string $license_key
20618 *
20619 * @return \FS_Plugin_License|object
20620 */
20621 private function fetch_license_by_key( $license_id, $license_key ) {
20622 $this->_logger->entrance();
20623
20624 $api = $this->get_current_or_network_user_api_scope();
20625
20626 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20627
20628 return $this->is_api_result_entity( $result ) ?
20629 new FS_Plugin_License( $result ) :
20630 $result;
20631 }
20632
20633 /**
20634 * @author Vova Feldman (@svovaf)
20635 * @since 1.2.0
20636 * @uses FS_Api
20637 *
20638 * @param number|bool $plugin_id
20639 * @param bool $flush
20640 *
20641 * @return FS_Payment[]|object
20642 */
20643 function _fetch_payments( $plugin_id = false, $flush = false ) {
20644 $this->_logger->entrance();
20645
20646 $api = $this->get_api_user_scope();
20647
20648 if ( ! is_numeric( $plugin_id ) ) {
20649 $plugin_id = $this->_plugin->id;
20650 }
20651
20652 $include_bundles = (
20653 is_object( $this->_plugin ) &&
20654 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20655 );
20656
20657 $result = $api->get(
20658 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20659 $flush
20660 );
20661
20662 if ( ! isset( $result->error ) ) {
20663 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20664 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20665 }
20666 $result = $result->payments;
20667 }
20668
20669 return $result;
20670 }
20671
20672 /**
20673 * @author Vova Feldman (@svovaf)
20674 * @since 1.2.1.5
20675 * @uses FS_Api
20676 *
20677 * @param bool $flush
20678 *
20679 * @return \FS_Billing|mixed
20680 */
20681 function _fetch_billing( $flush = false ) {
20682 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20683
20684 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20685
20686 if ( $this->is_api_result_entity( $billing ) ) {
20687 $billing = new FS_Billing( $billing );
20688 }
20689
20690 return $billing;
20691 }
20692
20693 /**
20694 * @author Vova Feldman (@svovaf)
20695 * @since 1.0.5
20696 *
20697 * @param FS_Plugin_License[] $licenses
20698 * @param number $module_id
20699 */
20700 private function _update_licenses( $licenses, $module_id ) {
20701 $this->_logger->entrance();
20702
20703 if ( is_array( $licenses ) ) {
20704 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20705 $licenses[ $i ]->updated = time();
20706 }
20707 }
20708
20709 $this->_store_licenses( true, $module_id, $licenses );
20710 }
20711
20712 /**
20713 * @author Vova Feldman (@svovaf)
20714 * @since 1.0.4
20715 *
20716 * @param bool|number $plugin_id
20717 * @param bool $flush Since 1.1.7.3
20718 * @param int $expiration Since 1.2.2.7
20719 * @param bool|string $newer_than Since 2.2.1
20720 *
20721 * @return object|false New plugin tag info if exist.
20722 */
20723 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20724 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20725
20726 if ( ! is_object( $latest_tag ) ) {
20727 return false;
20728 }
20729
20730 $plugin_version = $this->get_plugin_version();
20731
20732 // Check if version is actually newer.
20733 $has_new_version =
20734 // If it's an non-installed add-on then always return latest.
20735 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20736 // Compare versions.
20737 version_compare( $plugin_version, $latest_tag->version, '<' );
20738
20739 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20740
20741 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20742
20743 $this->_storage->beta_data = array(
20744 'is_beta' => $is_latest_version_beta,
20745 'version' => $latest_tag->version
20746 );
20747
20748 return $has_new_version ? $latest_tag : false;
20749 }
20750
20751 /**
20752 * @author Vova Feldman (@svovaf)
20753 * @since 1.0.5
20754 *
20755 * @param bool|number $plugin_id
20756 * @param bool $flush Since 1.1.7.3
20757 * @param int $expiration Since 1.2.2.7
20758 * @param bool|string $newer_than Since 2.2.1
20759 *
20760 * @return bool|FS_Plugin_Tag
20761 */
20762 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20763 $this->_logger->entrance();
20764
20765 if ( ! is_numeric( $plugin_id ) ) {
20766 $plugin_id = $this->_plugin->id;
20767 }
20768
20769 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20770 $updates = $this->get_all_updates();
20771
20772 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20773 }
20774
20775 /**
20776 * Check if site assigned with active license.
20777 *
20778 * @author Vova Feldman (@svovaf)
20779 * @since 1.0.6
20780 *
20781 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20782 */
20783 function has_active_license() {
20784 return (
20785 is_object( $this->_license ) &&
20786 is_numeric( $this->_license->id ) &&
20787 ! $this->_license->is_expired()
20788 );
20789 }
20790
20791 /**
20792 * Check if site assigned with active & valid (not expired) license.
20793 *
20794 * @author Vova Feldman (@svovaf)
20795 * @since 1.2.1
20796 *
20797 * @param bool $check_expiration
20798 */
20799 function has_active_valid_license( $check_expiration = true ) {
20800 return self::is_active_valid_license( $this->_license, $check_expiration );
20801 }
20802
20803 /**
20804 * @author Leo Fajardo (@leorw)
20805 * @since 2.3.1
20806 */
20807 function is_data_debug_mode() {
20808 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20809 return false;
20810 }
20811
20812 $fs = $this->is_addon() ?
20813 $this->get_parent_instance() :
20814 $this;
20815
20816 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20817 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20818 } else {
20819 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20820 }
20821
20822 return ( 'true' === $is_developer_license_debug_mode );
20823 }
20824
20825 /**
20826 * @author Leo Fajardo (@leorw)
20827 * @since 2.3.1
20828 */
20829 function _set_data_debug_mode() {
20830 if ( ! $this->is_whitelabeled( true ) ) {
20831 return;
20832 }
20833
20834 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20835
20836 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20837 'true' :
20838 'false';
20839
20840 if ( 'true' === $transient_value ) {
20841 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20842 'last_license_key' :
20843 'last_license_user_key'
20844 );
20845
20846 if ( md5( $license_or_user_key ) !== $stored_key ) {
20847 $this->shoot_ajax_failure( sprintf(
20848 '%s... %s',
20849 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20850 $this->get_text_inline(
20851 'seems like the key you entered doesn\'t match our records.',
20852 'developer-or-license-not-found'
20853 )
20854 ) );
20855 }
20856 }
20857
20858 if ( $this->is_network_active() && fs_is_network_admin() ) {
20859 set_site_transient(
20860 "fs_{$this->get_id()}_data_debug_mode",
20861 $transient_value,
20862 WP_FS__TIME_24_HOURS_IN_SEC / 24
20863 );
20864 } else {
20865 set_transient(
20866 "fs_{$this->get_id()}_data_debug_mode",
20867 $transient_value,
20868 WP_FS__TIME_24_HOURS_IN_SEC / 24
20869 );
20870 }
20871
20872 if ( 'true' === $transient_value ) {
20873 $this->_admin_notices->add_sticky(
20874 $this->get_text_inline(
20875 '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.',
20876 'data_debug_mode_enabled'
20877 ),
20878 'data_debug_mode_enabled'
20879 );
20880 }
20881
20882 $this->shoot_ajax_success();
20883 }
20884
20885 /**
20886 * Check if a given license is active & valid (not expired).
20887 *
20888 * @author Vova Feldman (@svovaf)
20889 * @since 2.1.3
20890 *
20891 * @param FS_Plugin_License $license
20892 * @param bool $check_expiration
20893 *
20894 * @return bool
20895 */
20896 private static function is_active_valid_license( $license, $check_expiration = true ) {
20897 return (
20898 is_object( $license ) &&
20899 FS_Plugin_License::is_valid_id( $license->id ) &&
20900 $license->is_active() &&
20901 ( ! $check_expiration || $license->is_valid() )
20902 );
20903 }
20904
20905 /**
20906 * Checks if there's any site that is associated with an active & valid license.
20907 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20908 *
20909 * @author Vova Feldman (@svovaf)
20910 * @since 2.1.3
20911 *
20912 * @return bool
20913 */
20914 function has_any_active_valid_license() {
20915 if ( ! fs_is_network_admin() ) {
20916 return $this->has_active_valid_license();
20917 }
20918
20919 $installs = $this->get_blog_install_map();
20920 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20921
20922 foreach ( $installs as $blog_id => $install ) {
20923 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20924 continue;
20925 }
20926
20927 foreach ( $all_plugin_licenses as $license ) {
20928 if ( $license->id == $install->license_id ) {
20929 if ( self::is_active_valid_license( $license ) ) {
20930 return true;
20931 }
20932 }
20933 }
20934 }
20935
20936 return false;
20937 }
20938
20939 /**
20940 * Check if site assigned with license with enabled features.
20941 *
20942 * @author Vova Feldman (@svovaf)
20943 * @since 1.0.6
20944 *
20945 * @return bool
20946 */
20947 function has_features_enabled_license() {
20948 return (
20949 is_object( $this->_license ) &&
20950 is_numeric( $this->_license->id ) &&
20951 $this->_license->is_features_enabled()
20952 );
20953 }
20954
20955 /**
20956 * Checks if the product is activated with a bundle license.
20957 *
20958 * @author Leo Fajardo (@leorw)
20959 * @since 2.4.0
20960 *
20961 * @return bool
20962 */
20963 function is_activated_with_bundle_license() {
20964 if ( ! $this->has_features_enabled_license() ) {
20965 return false;
20966 }
20967
20968 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20969 }
20970
20971 /**
20972 * Check if user is a trial or have feature enabled license.
20973 *
20974 * @author Vova Feldman (@svovaf)
20975 * @since 1.1.7
20976 *
20977 * @return bool
20978 */
20979 function can_use_premium_code() {
20980 return $this->is_trial() || $this->has_features_enabled_license();
20981 }
20982
20983 /**
20984 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20985 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20986 * the context user is authenticated.
20987 *
20988 * @author Leo Fajardo (@leorw)
20989 * @since 1.2.2
20990 *
20991 * @return bool
20992 */
20993 function is_user_admin() {
20994 /**
20995 * Require a super-admin when network activated, running from the network level OR if
20996 * running from the site level but not delegated the opt-in.
20997 *
20998 * @author Vova Feldman (@svovaf)
20999 * @since 2.0.0
21000 */
21001 if ( $this->_is_network_active &&
21002 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
21003 ) {
21004 return is_super_admin();
21005 }
21006
21007 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
21008 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
21009 }
21010
21011 /**
21012 * Sync site's plan.
21013 *
21014 * @author Vova Feldman (@svovaf)
21015 * @since 1.0.3
21016 *
21017 * @uses FS_Api
21018 *
21019 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
21020 * the admin.
21021 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
21022 * network-level "Account" page.
21023 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
21024 * `_sync_plugin_license` method in order to switch to the previous blog when sending
21025 * updates for a single site in case `execute_cron` has switched to a different blog.
21026 */
21027 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
21028 $this->_logger->entrance();
21029
21030 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21031
21032 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
21033
21034 if ( $is_addon_sync ) {
21035 $this->_sync_addon_license( $plugin_id, $background );
21036 } else {
21037 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
21038 }
21039
21040 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
21041 }
21042
21043 /**
21044 * Sync plugin's add-on license.
21045 *
21046 * @author Vova Feldman (@svovaf)
21047 * @since 1.0.6
21048 * @uses FS_Api
21049 *
21050 * @param number $addon_id
21051 * @param bool $background
21052 */
21053 private function _sync_addon_license( $addon_id, $background ) {
21054 $this->_logger->entrance();
21055
21056 if ( $this->is_addon_activated( $addon_id ) ) {
21057 // If already installed, use add-on sync.
21058 $fs_addon = self::get_instance_by_id( $addon_id );
21059
21060 if (
21061 // Add-on is network activated and network integrated.
21062 $fs_addon->is_network_active() ||
21063 // Background sync cron.
21064 self::is_cron() ||
21065 // Add-on is not network activated or not network integrated.
21066 ! fs_is_network_admin()
21067 ) {
21068 $fs_addon->_sync_license( $background );
21069
21070 return;
21071 }
21072 }
21073
21074 // Validate add-on exists.
21075 $addon = $this->get_addon( $addon_id );
21076
21077 if ( ! is_object( $addon ) ) {
21078 return;
21079 }
21080
21081 // Add add-on into account add-ons.
21082 $account_addons = $this->get_account_addons();
21083 if ( ! is_array( $account_addons ) ) {
21084 $account_addons = array();
21085 }
21086 $account_addons[] = $addon->id;
21087 $account_addons = array_unique( $account_addons );
21088 $this->_store_account_addons( $account_addons );
21089
21090 // Load add-on licenses.
21091 $licenses = $this->_fetch_licenses( $addon->id );
21092
21093 // Sync add-on licenses.
21094 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21095 $this->_update_licenses( $licenses, $addon->id );
21096
21097 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21098 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21099
21100 if ( ! isset( $plans_result->error ) ) {
21101 $plans = array();
21102 foreach ( $plans_result->plans as $plan ) {
21103 $plans[] = new FS_Plugin_Plan( $plan );
21104 }
21105
21106 $this->_admin_notices->add_sticky(
21107 sprintf(
21108 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21109 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21110 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21111 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21112 $addon->title
21113 ) . ' ' . $this->get_latest_download_link(
21114 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21115 $addon_id
21116 ),
21117 'addon_plan_upgraded_' . $addon->slug,
21118 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21119 );
21120 }
21121 }
21122 }
21123 }
21124
21125 /**
21126 * Sync site's plugin plan.
21127 *
21128 * @author Vova Feldman (@svovaf)
21129 * @since 1.0.6
21130 * @uses FS_Api
21131 *
21132 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21133 * @param bool $send_installs_update Since 2.0.0
21134 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21135 * syncing its license from the network-level "Account" page (e.g.: after
21136 * activating a license only for the single install).
21137 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21138 * can be used here to switch to the previous blog in case `execute_cron`
21139 * has switched to a different blog.
21140 */
21141 private function _sync_plugin_license(
21142 $background = false,
21143 $send_installs_update = true,
21144 $is_context_single_site = false,
21145 $current_blog_id = null
21146 ) {
21147 $this->_logger->entrance();
21148
21149 $plan_change = 'none';
21150
21151 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21152
21153 if ( ! $send_installs_update ) {
21154 $site = $this->_site;
21155 } else {
21156 /**
21157 * Sync site info.
21158 *
21159 * @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.
21160 */
21161 if ( $is_site_level_sync ) {
21162 /**
21163 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21164 *
21165 * @author Leo Fajardo (@leorw)
21166 * @since 2.2.3
21167 */
21168 if ( is_numeric( $current_blog_id ) ) {
21169 $this->switch_to_blog( $current_blog_id );
21170 }
21171
21172 $result = $this->send_install_update( array(), true, true );
21173 $is_valid = $this->is_api_result_entity( $result );
21174 } else {
21175 $result = $this->send_installs_update( array(), true, true );
21176 $is_valid = $this->is_api_result_object( $result, 'installs' );
21177 }
21178
21179 if ( ! $is_valid ) {
21180 if ( $is_context_single_site ) {
21181 // Switch back to the main blog so that the following logic will have the right entities.
21182 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21183 }
21184
21185 // Show API message only if not background sync or if paying customer.
21186 if ( ! $background || $this->is_paying() ) {
21187 // Try to ping API to see if not blocked.
21188 if ( FS_Api::is_blocked( $result ) ) {
21189 /**
21190 * @author Vova Feldman (@svovaf)
21191 * @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.
21192 */
21193 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21194 // Add notice immediately if not a background sync.
21195 $add_notice = ( ! $background );
21196
21197 if ( ! $add_notice ) {
21198 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21199
21200 // We only want to add the notice after 3 consecutive failures.
21201 $add_notice = ( 3 <= $counter );
21202
21203 if ( ! $add_notice ) {
21204 /**
21205 * 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.
21206 *
21207 * 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.
21208 */
21209 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21210 }
21211 }
21212
21213 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21214 if ( $add_notice ) {
21215 self::$_global_admin_notices->add(
21216 $this->generate_api_blocked_notice_message_from_result( $result ),
21217 '',
21218 'error',
21219 $background,
21220 'api_blocked'
21221 );
21222
21223 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21224
21225 // Notice was just shown, reset connectivity counter.
21226 delete_transient( '_fs_api_connection_retry_counter' );
21227 }
21228 }
21229 } else if ( is_object( $result ) ) {
21230 // Authentication params are broken.
21231 $this->_admin_notices->add(
21232 $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 ),
21233 '',
21234 'error'
21235 );
21236 }
21237 }
21238
21239 // No reason to continue with license sync while there are API issues.
21240 return;
21241 }
21242
21243 // API is working now. Delete the transient and start afresh.
21244 delete_transient('_fs_api_connection_retry_counter');
21245
21246 if ( $is_site_level_sync ) {
21247 $site = new FS_Site( $result );
21248 } else {
21249 // Map site addresses to their blog IDs.
21250 $address_to_blog_map = $this->get_address_to_blog_map();
21251
21252 // Find the current context install.
21253 $site = null;
21254 foreach ( $result->installs as $install ) {
21255 if ( $install->id == $this->_site->id ) {
21256 $site = new FS_Site( $install );
21257 } else {
21258 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21259 $blog_id = $address_to_blog_map[ $address ];
21260
21261 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21262 }
21263 }
21264 }
21265
21266 // Sync plans.
21267 $this->_sync_plans();
21268 }
21269
21270 // Remove sticky API connectivity message.
21271 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21272
21273 if ( ! $this->has_paid_plan() ) {
21274 $this->_site = $site;
21275 $this->_store_site(
21276 true,
21277 $is_site_level_sync ?
21278 null :
21279 $this->get_network_install_blog_id()
21280 );
21281 } else {
21282 $context_blog_id = 0;
21283
21284 if ( $is_context_single_site ) {
21285 $context_blog_id = get_current_blog_id();
21286
21287 // Switch back to the main blog in order to properly sync the license.
21288 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21289 }
21290
21291 /**
21292 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21293 * associated with that ID is not included in the user's licenses collection.
21294 */
21295 $this->_sync_licenses(
21296 $site->license_id,
21297 ( $is_context_single_site ?
21298 $context_blog_id :
21299 null
21300 )
21301 );
21302
21303 if ( $is_context_single_site ) {
21304 $this->switch_to_blog( $context_blog_id );
21305 }
21306
21307 // Check if plan / license changed.
21308 if ( $site->plan_id != $this->_site->plan_id ||
21309 // Check if trial started.
21310 $site->trial_plan_id != $this->_site->trial_plan_id ||
21311 $site->trial_ends != $this->_site->trial_ends ||
21312 // Check if license changed.
21313 $site->license_id != $this->_site->license_id
21314 ) {
21315 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21316 // New trial started.
21317 $this->_site = $site;
21318 $plan_change = 'trial_started';
21319
21320 // For trial with subscription use-case.
21321 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21322
21323 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21324 $this->_site = $site;
21325 $this->_update_site_license( $new_license );
21326 $this->_store_licenses();
21327
21328 $this->_sync_site_subscription( $this->_license );
21329 }
21330 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21331 // Was in trial, but now trial expired and no license ID.
21332 // New trial started.
21333 $this->_site = $site;
21334 $plan_change = 'trial_expired';
21335 } else {
21336 $is_free = $this->is_free_plan();
21337
21338 // Make sure license exist and not expired.
21339 $new_license = is_null( $site->license_id ) ?
21340 null :
21341 $this->_get_license_by_id( $site->license_id );
21342
21343 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21344 // License cancelled.
21345 $this->_site = $site;
21346 $this->_update_site_license( $new_license );
21347 $this->_store_licenses();
21348
21349 $plan_change = 'cancelled';
21350 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21351 // The license is expired, so ignore upgrade method.
21352 $this->_site = $site;
21353 } else {
21354 // License changed.
21355 $this->_site = $site;
21356
21357 /**
21358 * IMPORTANT:
21359 * 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.
21360 *
21361 * @author Vova Feldman (@svovaf)
21362 * @since 2.0.0
21363 */
21364 $this->_update_site_license( $new_license );
21365
21366 if ( ! $is_context_single_site &&
21367 fs_is_network_admin() &&
21368 $this->_is_network_active &&
21369 $new_license->quota > 1 &&
21370 get_blog_count() > 1
21371 ) {
21372 // See if license can activated on all sites.
21373 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21374 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21375 // Open the license activation dialog box on the account page.
21376 add_action( 'admin_footer', array(
21377 &$this,
21378 '_open_license_activation_dialog_box'
21379 ) );
21380 }
21381 }
21382 }
21383
21384 $this->_store_licenses();
21385
21386 $plan_change = $is_free ?
21387 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21388 ( is_object( $new_license ) ?
21389 'changed' :
21390 'downgraded' );
21391 }
21392 }
21393
21394 // Store updated site info.
21395 $this->_store_site(
21396 true,
21397 $is_site_level_sync ?
21398 null :
21399 $this->get_network_install_blog_id()
21400 );
21401 } else {
21402 if ( ! is_object( $this->_license ) ) {
21403 $this->maybe_update_whitelabel_flag(
21404 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21405 $this->get_license_by_id( $site->license_id ) :
21406 null
21407 );
21408 } else {
21409 $this->maybe_update_whitelabel_flag( $this->_license );
21410
21411 if ( $this->_license->is_expired() ) {
21412 if ( ! $this->has_features_enabled_license() ) {
21413 $this->_deactivate_license();
21414 $plan_change = 'downgraded';
21415 } else {
21416 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21417
21418 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21419 /**
21420 * Show the expired license notice every 14 days.
21421 *
21422 * @author Leo Fajardo (@leorw)
21423 * @since 2.3.1
21424 */
21425 $plan_change = 'expired';
21426 }
21427 }
21428 }
21429 }
21430
21431 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21432 $this->_sync_site_subscription( $this->_license );
21433 }
21434 }
21435
21436 if ( ! $this->is_addon() &&
21437 $this->_site->is_beta() !== $site->is_beta()
21438 ) {
21439 // Beta flag updated.
21440 $this->_site = $site;
21441
21442 $this->_store_site(
21443 true,
21444 $is_site_level_sync ?
21445 null :
21446 $this->get_network_install_blog_id()
21447 );
21448 }
21449
21450 if ( $this->is_addon() || $this->has_addons() ) {
21451 /**
21452 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21453 * an updated valid user licenses collection will be fetched from the server which is used to also
21454 * update the account add-ons (add-ons the user has licenses for).
21455 *
21456 * @author Leo Fajardo (@leorw)
21457 * @since 2.2.4
21458 */
21459 $this->purge_valid_user_licenses_cache();
21460 }
21461 }
21462
21463 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21464
21465 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21466 switch ( $plan_change ) {
21467 case 'none':
21468 if ( ! $background && is_admin() ) {
21469 $plan = $this->is_trial() ?
21470 $this->get_trial_plan() :
21471 $this->get_plan();
21472
21473 if ( $plan->is_free() ) {
21474 $this->_admin_notices->add(
21475 sprintf(
21476 $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' ),
21477 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21478 ) . ' ' . sprintf(
21479 '<a href="%s">%s</a>',
21480 $this->contact_url(
21481 'bug',
21482 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' ),
21483 strtoupper( $plan->name )
21484 )
21485 ),
21486 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21487 ),
21488 $hmm_text
21489 );
21490 }
21491 }
21492 break;
21493 case 'upgraded':
21494 case 'activated':
21495 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21496
21497 $this->_admin_notices->remove_sticky( array(
21498 'trial_started',
21499 'trial_promotion',
21500 'trial_expired',
21501 'activation_complete',
21502 'license_expired',
21503 ) );
21504 break;
21505 case 'changed':
21506 $this->_admin_notices->add_sticky(
21507 sprintf(
21508 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21509 $this->get_plan_title()
21510 ),
21511 'plan_changed'
21512 );
21513
21514 $this->_admin_notices->remove_sticky( array(
21515 'trial_started',
21516 'trial_promotion',
21517 'trial_expired',
21518 'activation_complete',
21519 ) );
21520 break;
21521 case 'downgraded':
21522 $this->_admin_notices->add_sticky(
21523 ($this->has_free_plan() ?
21524 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 ) :
21525 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21526 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) ) ),
21527 'license_expired',
21528 $hmm_text
21529 );
21530 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21531 break;
21532 case 'cancelled':
21533 $this->_admin_notices->add(
21534 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21535 sprintf(
21536 '<a href="%s">%s</a>',
21537 $this->contact_url( 'bug' ),
21538 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21539 ),
21540 $hmm_text,
21541 'error'
21542 );
21543 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21544 break;
21545 case 'expired':
21546 $this->_admin_notices->add_sticky(
21547 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 ),
21548 'license_expired',
21549 $hmm_text
21550 );
21551
21552 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21553
21554 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21555 break;
21556 case 'trial_started':
21557 $this->add_complete_upgrade_instructions_notice(
21558 sprintf(
21559 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21560 '<i>' . $this->get_plugin_name() . '</i>'
21561 ),
21562 'trial_started',
21563 $this->get_trial_plan()->title
21564 );
21565
21566 $this->_admin_notices->remove_sticky( array(
21567 'trial_promotion',
21568 ) );
21569 break;
21570 case 'trial_expired':
21571 $this->_admin_notices->add_sticky(
21572 ($this->has_free_plan() ?
21573 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21574 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21575 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))),
21576 'trial_expired',
21577 $hmm_text
21578 );
21579 $this->_admin_notices->remove_sticky( array(
21580 'trial_started',
21581 'trial_promotion',
21582 'plan_upgraded',
21583 ) );
21584 break;
21585 }
21586 }
21587
21588 if ( 'none' !== $plan_change ) {
21589 if (
21590 ! is_object( $this->_license ) ||
21591 ! $this->_license->is_whitelabeled
21592 ) {
21593 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21594 }
21595
21596 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21597 }
21598 }
21599
21600 /**
21601 * @author Leo Fajardo (@leorw)
21602 * @since 2.5.4
21603 *
21604 * @param mixed $result
21605 *
21606 * @return string
21607 */
21608 private function generate_api_blocked_notice_message_from_result( $result ) {
21609 $api_domains = $this->apply_filters( 'api_domains', array(
21610 'api.freemius.com',
21611 'wp.freemius.com',
21612 ) );
21613
21614 $api_domains_list_items = '';
21615
21616 foreach( $api_domains as $api_domain ) {
21617 $api_domains_list_items .= "<li>{$api_domain}</li>";
21618 }
21619
21620 $error_message = sprintf(
21621 $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' ),
21622 $this->get_plugin_name(),
21623 "<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>"
21624 );
21625
21626 $error_message =
21627 "<div>{$error_message}</div>" .
21628 '<div class="fs-api-request-error-details" style="display: none">' .
21629 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21630 $result->error->message .
21631 '</div>';
21632
21633 return $error_message;
21634 }
21635
21636 /**
21637 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21638 *
21639 * @author Vova Feldman (@svovaf)
21640 * @since 2.0.0
21641 */
21642 public function _open_license_activation_dialog_box() {
21643 $vars = array( 'license_id' => $this->_site->license_id );
21644 fs_require_once_template( 'js/open-license-activation.php', $vars );
21645 }
21646
21647 /**
21648 * @author Vova Feldman (@svovaf)
21649 * @since 1.0.5
21650 *
21651 * @param bool $background
21652 * @param FS_Plugin_License|null $premium_license
21653 */
21654 protected function _activate_license( $background = false, $premium_license = null ) {
21655 $this->_logger->entrance();
21656
21657 if ( is_null( $premium_license ) ) {
21658 $license_id = fs_request_get( 'license_id' );
21659
21660 if ( is_object( $this->_site ) &&
21661 FS_Plugin_License::is_valid_id( $license_id ) &&
21662 $license_id == $this->_site->license_id
21663 ) {
21664 // License is already activated.
21665 return;
21666 }
21667
21668 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21669 $this->_get_license_by_id( $license_id ) :
21670 $this->_get_available_premium_license();
21671 }
21672
21673 if ( ! is_object( $premium_license ) ) {
21674 return;
21675 }
21676
21677 if ( ! is_object( $this->_site ) ) {
21678 // Not yet opted-in.
21679 $user = $this->get_current_or_network_user();
21680 if ( ! is_object( $user ) ) {
21681 $user = self::_get_user_by_id( $premium_license->user_id );
21682 }
21683
21684 if ( is_object( $user ) ) {
21685 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21686 } else {
21687 $this->opt_in(
21688 false,
21689 false,
21690 false,
21691 $premium_license->secret_key
21692 );
21693
21694 return;
21695 }
21696 }
21697
21698
21699 /**
21700 * If the premium license is already associated with the install, just
21701 * update the license reference (activation is not required).
21702 *
21703 * @since 1.1.9
21704 */
21705 if ( $premium_license->id == $this->_site->license_id ) {
21706 // License is already activated.
21707 $this->_update_site_license( $premium_license );
21708 $this->_store_account();
21709
21710 return;
21711 }
21712
21713 if ( $this->_site->user_id != $premium_license->user_id ) {
21714 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21715 } else {
21716 $api_request_params = array();
21717 }
21718
21719 $api = $this->get_api_site_scope();
21720 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21721
21722 if ( ! $this->is_api_result_entity( $license ) ) {
21723 if ( ! $background ) {
21724 $this->_admin_notices->add( sprintf(
21725 '%s %s',
21726 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21727 ( is_object( $license ) && isset( $license->error ) ?
21728 $license->error->message :
21729 sprintf( '%s<br><code>%s</code>',
21730 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21731 var_export( $license, true )
21732 )
21733 )
21734 ),
21735 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21736 'error'
21737 );
21738 }
21739
21740 return;
21741 }
21742
21743 $premium_license = new FS_Plugin_License( $license );
21744
21745 // Updated site plan.
21746 $site = $this->get_api_site_scope()->get( '/', true );
21747 if ( $this->is_api_result_entity( $site ) ) {
21748 $this->_site = new FS_Site( $site );
21749 }
21750 $this->_update_site_license( $premium_license );
21751
21752 $this->_store_account();
21753
21754 if ( $this->is_addon() || $this->has_addons() ) {
21755 /**
21756 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21757 * an updated valid user licenses collection will be fetched from the server which is used to also
21758 * update the account add-ons (add-ons the user has licenses for).
21759 *
21760 * @author Leo Fajardo (@leorw)
21761 * @since 2.2.4
21762 */
21763 $this->purge_valid_user_licenses_cache();
21764 }
21765
21766 if ( ! $background ) {
21767 $this->add_complete_upgrade_instructions_notice(
21768 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21769 'license_activated'
21770 );
21771 }
21772
21773 $this->_admin_notices->remove_sticky( array(
21774 'trial_promotion',
21775 'license_expired',
21776 ) );
21777 }
21778
21779 /**
21780 * @author Vova Feldman (@svovaf)
21781 * @since 1.0.5
21782 *
21783 * @param bool $show_notice
21784 */
21785 protected function _deactivate_license( $show_notice = true ) {
21786 $this->_logger->entrance();
21787
21788 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21789
21790 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21791 $this->_admin_notices->add(
21792 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() ),
21793 $hmm_text
21794 );
21795
21796 return;
21797 }
21798
21799 $api = $this->get_api_site_scope();
21800 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21801
21802 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21803 }
21804
21805 /**
21806 * @author Leo Fajardo (@leorw)
21807 * @since 2.2.1
21808 *
21809 * @param FS_Plugin_License $license
21810 * @param bool|string $hmm_text
21811 * @param bool $show_notice
21812 */
21813 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21814 if ( isset( $license->error ) ) {
21815 $this->_admin_notices->add(
21816 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21817 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21818 $hmm_text,
21819 'error'
21820 );
21821
21822 return;
21823 }
21824
21825 // Update license cache.
21826 if ( is_array( $this->_licenses ) ) {
21827 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21828 if ( $license->id == $this->_licenses[ $i ]->id ) {
21829 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21830 }
21831 }
21832 }
21833
21834 // Update site plan to default.
21835 $this->_sync_plans();
21836 $this->_site->plan_id = $this->_plans[0]->id;
21837 // Unlink license from site.
21838 $this->_update_site_license( null );
21839
21840 $this->_store_account();
21841
21842 if ( $show_notice ) {
21843 $this->_admin_notices->add(
21844 sprintf( $this->is_only_premium() ?
21845 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21846 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21847 $this->get_plan_title()
21848 ),
21849 $this->get_text_inline( 'O.K', 'ok' )
21850 );
21851 }
21852
21853 $this->_admin_notices->remove_sticky( array(
21854 'plan_upgraded',
21855 'license_activated',
21856 ) );
21857 }
21858
21859 /**
21860 * Site plan downgrade.
21861 *
21862 * @author Vova Feldman (@svovaf)
21863 * @since 1.0.4
21864 *
21865 * @return object
21866 *
21867 * @uses FS_Api
21868 */
21869 private function _downgrade_site() {
21870 $this->_logger->entrance();
21871
21872 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21873
21874 $api = $this->get_api_site_scope();
21875 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21876
21877 $plan_downgraded = false;
21878 $plan = false;
21879 if ( $this->is_api_result_entity( $site ) ) {
21880 $prev_plan_id = $this->_site->plan_id;
21881
21882 // Update new site plan id.
21883 $this->_site->plan_id = $site->plan_id;
21884
21885 $plan = $this->get_plan();
21886 $subscription = $this->_sync_site_subscription( $this->_license );
21887
21888 // Plan downgraded if plan was changed or subscription was cancelled.
21889 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21890 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21891 } else {
21892 // handle different error cases.
21893 $this->handle_license_deactivation_result(
21894 $site,
21895 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21896 );
21897 }
21898
21899 if ( ! $plan_downgraded ) {
21900 return (object) array(
21901 'error' => (object) array(
21902 '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' )
21903 )
21904 );
21905 }
21906
21907 // Remove previous sticky message about upgrade (if exist).
21908 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21909
21910 $this->_admin_notices->add(
21911 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21912 $plan->title,
21913 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21914 )
21915 );
21916
21917 // Store site updates.
21918 $this->_store_site();
21919
21920 if ( $deactivate_license &&
21921 ! FS_Plugin_License::is_valid_id( $site->license_id )
21922 ) {
21923 if ( $this->_site->is_localhost() ) {
21924 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21925 } else {
21926 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21927 }
21928
21929 // Handle successful license deactivation result.
21930 $this->handle_license_deactivation_result( $this->_license );
21931 }
21932
21933 return $site;
21934 }
21935
21936 /**
21937 * @author Vova Feldman (@svovaf)
21938 * @since 1.1.8.1
21939 *
21940 * @param bool|string $plan_name
21941 *
21942 * @return bool If trial was successfully started.
21943 */
21944 function start_trial( $plan_name = false ) {
21945 $this->_logger->entrance();
21946
21947 // Alias.
21948 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21949
21950 if ( $this->is_trial() ) {
21951 // Already in trial mode.
21952 $this->_admin_notices->add(
21953 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21954 $oops_text,
21955 'error'
21956 );
21957
21958 return false;
21959 }
21960
21961 if ( $this->_site->is_trial_utilized() ) {
21962 // Trial was already utilized.
21963 $this->_admin_notices->add(
21964 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21965 $oops_text,
21966 'error'
21967 );
21968
21969 return false;
21970 }
21971
21972 if ( false !== $plan_name ) {
21973 $plan = $this->get_plan_by_name( $plan_name );
21974
21975 if ( false === $plan ) {
21976 // Plan doesn't exist.
21977 $this->_admin_notices->add(
21978 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21979 $oops_text,
21980 'error'
21981 );
21982
21983 return false;
21984 }
21985
21986 if ( ! $plan->has_trial() ) {
21987 // Plan doesn't exist.
21988 $this->_admin_notices->add(
21989 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21990 $oops_text,
21991 'error'
21992 );
21993
21994 return false;
21995 }
21996 } else {
21997 if ( ! $this->has_trial_plan() ) {
21998 // None of the plans have a trial.
21999 $this->_admin_notices->add(
22000 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
22001 $oops_text,
22002 'error'
22003 );
22004
22005 return false;
22006 }
22007
22008 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
22009
22010 $plan = $plans_with_trial[0];
22011 }
22012
22013 $api = $this->get_api_site_scope();
22014 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
22015
22016 if ( ! $this->is_api_result_entity( $plan ) ) {
22017 // Some API error while trying to start the trial.
22018 $this->_admin_notices->add(
22019 $this->get_api_error_message( $plan ),
22020 $oops_text,
22021 'error'
22022 );
22023
22024 return false;
22025 }
22026
22027 // Sync license.
22028 $this->_sync_license();
22029
22030 return $this->is_trial();
22031 }
22032
22033 /**
22034 * Cancel site trial.
22035 *
22036 * @author Vova Feldman (@svovaf)
22037 * @since 1.0.9
22038 *
22039 * @return object
22040 *
22041 * @uses FS_Api
22042 */
22043 private function _cancel_trial() {
22044 $this->_logger->entrance();
22045
22046 if ( ! $this->is_trial() ) {
22047 return (object) array(
22048 'error' => (object) array(
22049 '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' )
22050 )
22051 );
22052 }
22053
22054 $trial_plan = $this->get_trial_plan();
22055
22056 $api = $this->get_api_site_scope();
22057 $site = $api->call( 'trials.json', 'delete' );
22058
22059 $trial_cancelled = false;
22060
22061 if ( $this->is_api_result_entity( $site ) ) {
22062 $prev_trial_ends = $this->_site->trial_ends;
22063
22064 if ( $this->is_paid_trial() ) {
22065 $this->_license->expiration = $site->trial_ends;
22066 $this->_license->is_cancelled = true;
22067 $this->_update_site_license( $this->_license );
22068 $this->_store_licenses();
22069
22070 // Clear subscription reference.
22071 $this->_sync_site_subscription( null );
22072 }
22073
22074 // Update site info.
22075 $this->_site = new FS_Site( $site );
22076
22077 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22078 } else {
22079 // @todo handle different error cases.
22080 }
22081
22082 if ( ! $trial_cancelled ) {
22083 return (object) array(
22084 'error' => (object) array(
22085 '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' )
22086 )
22087 );
22088 }
22089
22090 // Remove previous sticky messages about upgrade or trial (if exist).
22091 $this->_admin_notices->remove_sticky( array(
22092 'trial_started',
22093 'trial_promotion',
22094 'plan_upgraded',
22095 ) );
22096
22097 // Store site updates.
22098 $this->_store_site();
22099
22100 if ( ! $this->is_addon() ||
22101 ! $this->deactivate_premium_only_addon_without_license( true )
22102 ) {
22103 $this->_admin_notices->add(
22104 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22105 );
22106 }
22107
22108 return $site;
22109 }
22110
22111 /**
22112 * @author Vova Feldman (@svovaf)
22113 * @since 1.0.6
22114 *
22115 * @param bool|number $plugin_id
22116 *
22117 * @return bool
22118 */
22119 private function _is_addon_id( $plugin_id ) {
22120 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22121 }
22122
22123 /**
22124 * Check if user eligible to download premium version updates.
22125 *
22126 * @author Vova Feldman (@svovaf)
22127 * @since 1.0.6
22128 *
22129 * @return bool
22130 */
22131 private function _can_download_premium() {
22132 return $this->has_any_active_valid_license() ||
22133 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22134 }
22135
22136 /**
22137 *
22138 * @author Vova Feldman (@svovaf)
22139 * @since 1.0.6
22140 *
22141 * @param bool|number $addon_id
22142 * @param string $type "json" or "zip"
22143 *
22144 * @return string
22145 */
22146 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22147
22148 $is_addon = $this->_is_addon_id( $addon_id );
22149
22150 $is_premium = null;
22151 if ( ! $is_addon ) {
22152 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22153 } else if ( $this->is_addon_activated( $addon_id ) ) {
22154 $fs_addon = self::get_instance_by_id( $addon_id );
22155 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22156 }
22157
22158 // If add-on, then append add-on ID.
22159 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22160 '/updates/latest.' . $type;
22161
22162 // If add-on and not yet activated, try to fetch based on server licensing.
22163 if ( is_bool( $is_premium ) ) {
22164 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22165 }
22166
22167 if ( $this->has_secret_key() ) {
22168 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22169 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22170 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22171 }
22172
22173 return $endpoint;
22174 }
22175
22176 /**
22177 * @author Vova Feldman (@svovaf)
22178 * @since 1.0.4
22179 *
22180 * @param bool|number $addon_id
22181 * @param bool $flush Since 1.1.7.3
22182 * @param int $expiration Since 1.2.2.7
22183 * @param bool|string $newer_than Since 2.2.1
22184 * @param bool|string $fetch_readme Since 2.2.1
22185 *
22186 * @return object|false Plugin latest tag info.
22187 */
22188 function _fetch_latest_version(
22189 $addon_id = false,
22190 $flush = true,
22191 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22192 $newer_than = false,
22193 $fetch_readme = true
22194 ) {
22195 $this->_logger->entrance();
22196
22197 if ( $this->is_unresolved_clone( true ) ) {
22198 return false;
22199 }
22200
22201 $switch_to_blog_id = null;
22202
22203 /**
22204 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22205 * @since 1.1.7.4 Also check updates for add-ons.
22206 */
22207 if (
22208 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22209 ! $this->_is_addon_id( $addon_id )
22210 ) {
22211 if ( ! is_multisite() ) {
22212 return false;
22213 }
22214
22215 $installs_map = $this->get_blog_install_map();
22216
22217 foreach ( $installs_map as $blog_id => $install ) {
22218 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22219 continue;
22220 }
22221
22222 /**
22223 * @var FS_Site $install
22224 */
22225 if ( $install->is_trial() ) {
22226 $switch_to_blog_id = $blog_id;
22227 break;
22228 }
22229
22230 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22231 $license = $this->get_license_by_id( $install->license_id );
22232
22233 if ( is_object( $license ) && $license->is_features_enabled() ) {
22234 $switch_to_blog_id = $blog_id;
22235 break;
22236 }
22237 }
22238 }
22239
22240 if ( is_null( $switch_to_blog_id ) ) {
22241 return false;
22242 }
22243 }
22244
22245 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22246 get_current_blog_id() :
22247 0;
22248
22249 if ( is_numeric( $switch_to_blog_id ) ) {
22250 $this->switch_to_blog( $switch_to_blog_id );
22251 }
22252
22253 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22254
22255 if ( ! empty( $newer_than ) ) {
22256 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22257 }
22258
22259 if ( true === $fetch_readme ) {
22260 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22261 }
22262
22263 $tag = $this->get_api_site_or_plugin_scope()->get(
22264 $latest_version_endpoint,
22265 $flush,
22266 $expiration
22267 );
22268
22269 if ( is_numeric( $switch_to_blog_id ) ) {
22270 $this->switch_to_blog( $current_blog_id );
22271 }
22272
22273 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22274
22275 $this->_logger->departure( 'Latest version ' . $latest_version );
22276
22277 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22278 }
22279
22280 #----------------------------------------------------------------------------------
22281 #region Download Plugin
22282 #----------------------------------------------------------------------------------
22283
22284 /**
22285 * Download latest plugin version, based on plan.
22286 *
22287 * Not like _download_latest(), this will redirect the page
22288 * to secure download url to prevent dual download (from FS to WP server,
22289 * and then from WP server to the client / browser).
22290 *
22291 * @author Vova Feldman (@svovaf)
22292 * @since 1.0.9
22293 *
22294 * @param bool|number $plugin_id
22295 *
22296 * @uses FS_Api
22297 * @uses wp_redirect()
22298 */
22299 private function download_latest_directly( $plugin_id = false ) {
22300 $this->_logger->entrance();
22301
22302 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22303 }
22304
22305 /**
22306 * Get latest plugin FS API download URL.
22307 *
22308 * @author Vova Feldman (@svovaf)
22309 * @since 1.0.9
22310 *
22311 * @param bool|number $plugin_id
22312 *
22313 * @return string
22314 */
22315 private function get_latest_download_api_url( $plugin_id = false ) {
22316 $this->_logger->entrance();
22317
22318 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22319 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22320 );
22321
22322 return str_replace( 'http:', 'https:', $download_api_url );
22323 }
22324
22325 /**
22326 * Get payment invoice URL.
22327 *
22328 * @author Vova Feldman (@svovaf)
22329 * @since 1.2.0
22330 *
22331 * @param bool|number $payment_id
22332 *
22333 * @return string
22334 */
22335 function _get_invoice_api_url( $payment_id = false ) {
22336 $this->_logger->entrance();
22337
22338 $url = $this->get_api_user_scope()->get_signed_url(
22339 "/payments/{$payment_id}/invoice.pdf"
22340 );
22341
22342 if ( ! fs_starts_with( $url, 'https://' ) ) {
22343 // Always use HTTPS for invoices.
22344 $url = 'https' . substr( $url, 4 );
22345 }
22346
22347 return $url;
22348 }
22349
22350 /**
22351 * Get latest plugin download link.
22352 *
22353 * @author Vova Feldman (@svovaf)
22354 * @since 1.0.9
22355 *
22356 * @param string $label
22357 * @param bool|number $plugin_id
22358 *
22359 * @return string
22360 */
22361 private function get_latest_download_link( $label, $plugin_id = false ) {
22362 return sprintf(
22363 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22364 $this->_get_latest_download_local_url( $plugin_id ),
22365 $label
22366 );
22367 }
22368
22369 /**
22370 * Get latest plugin download local URL.
22371 *
22372 * @author Vova Feldman (@svovaf)
22373 * @since 1.0.9
22374 *
22375 * @param bool|number $plugin_id
22376 *
22377 * @return string
22378 */
22379 function _get_latest_download_local_url( $plugin_id = false ) {
22380 // Add timestamp to protect from caching.
22381 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22382
22383 if ( ! empty( $plugin_id ) ) {
22384 $params['plugin_id'] = $plugin_id;
22385 } else if ( $this->is_addon() ) {
22386 $params['plugin_id'] = $this->get_id();
22387 }
22388
22389 $fs = $this->is_addon() ?
22390 $this->get_parent_instance() :
22391 $this;
22392
22393 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22394 }
22395
22396 #endregion Download Plugin ------------------------------------------------------------------
22397
22398 /**
22399 * @author Vova Feldman (@svovaf)
22400 * @since 1.0.4
22401 *
22402 * @uses FS_Api
22403 *
22404 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22405 * was initiated by the admin.
22406 * @param bool|number $plugin_id
22407 * @param bool $flush Since 1.1.7.3
22408 * @param int $expiration Since 1.2.2.7
22409 * @param bool|string $newer_than Since 2.2.1
22410 */
22411 private function check_updates(
22412 $background = false,
22413 $plugin_id = false,
22414 $flush = true,
22415 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22416 $newer_than = false
22417 ) {
22418 $this->_logger->entrance();
22419
22420 // Check if there's a newer version for download.
22421 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22422
22423 $update = null;
22424 if ( is_object( $new_version ) ) {
22425 $update = new FS_Plugin_Tag( $new_version );
22426
22427 if ( ! $background ) {
22428 $this->_admin_notices->add(
22429 sprintf(
22430 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22431 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22432 $update->version,
22433 sprintf(
22434 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22435 $this->get_account_url( 'download_latest' ),
22436 sprintf(
22437 /* translators: %s: plan name (e.g. latest "Professional" version) */
22438 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22439 $this->get_plan_title()
22440 )
22441 )
22442 ),
22443 $this->get_text_inline( 'New', 'new' ) . '!'
22444 );
22445 }
22446 } else if ( false === $new_version && ! $background ) {
22447 $this->_admin_notices->add(
22448 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22449 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22450 );
22451 }
22452
22453 $this->_store_update( $update, true, $plugin_id );
22454 }
22455
22456 /**
22457 * @author Vova Feldman (@svovaf)
22458 * @since 1.0.4
22459 *
22460 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22461 *
22462 * @return FS_Plugin[]
22463 *
22464 * @uses FS_Api
22465 */
22466 private function sync_addons( $flush = false ) {
22467 $this->_logger->entrance();
22468
22469 $api = $this->get_api_site_or_plugin_scope();
22470
22471 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22472
22473 /**
22474 * @since 1.2.1
22475 *
22476 * If there's a cached version of the add-ons and not asking
22477 * for a flush, just use the currently stored add-ons.
22478 */
22479 if ( ! $flush && $api->is_cached( $path ) ) {
22480 $addons = self::get_all_addons();
22481
22482 return isset( $addons[ $this->_plugin->id ] ) ?
22483 $addons[ $this->_plugin->id ] :
22484 array();
22485 }
22486
22487 $result = $api->get( $path, $flush );
22488
22489 $addons = array();
22490 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22491 is_array( $result->plugins )
22492 ) {
22493 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22494 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22495 }
22496
22497 $this->_store_addons( $addons, true );
22498 }
22499
22500 return $addons;
22501 }
22502
22503 /**
22504 * Handle user email update.
22505 *
22506 * @author Vova Feldman (@svovaf)
22507 * @since 1.0.3
22508 * @uses FS_Api
22509 *
22510 * @param string $new_email
22511 *
22512 * @return object
22513 */
22514 private function update_email( $new_email ) {
22515 $this->_logger->entrance();
22516
22517 $api = $this->get_api_user_scope();
22518 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22519 'email' => $new_email,
22520 'after_email_confirm_url' => $this->_get_admin_page_url(
22521 'account',
22522 array( 'fs_action' => 'sync_user' )
22523 ),
22524 ) );
22525
22526 if ( ! isset( $user->error ) ) {
22527 $this->_user->email = $user->email;
22528 $this->_user->is_verified = $user->is_verified;
22529 $this->_store_user();
22530 } else {
22531 // handle different error cases.
22532 }
22533
22534 return $user;
22535 }
22536
22537 #----------------------------------------------------------------------------------
22538 #region API Error Handling
22539 #----------------------------------------------------------------------------------
22540
22541 /**
22542 * @author Vova Feldman (@svovaf)
22543 * @since 1.1.1
22544 *
22545 * @param mixed $result
22546 *
22547 * @return bool Is API result contains an error.
22548 */
22549 private function is_api_error( $result ) {
22550 return FS_Api::is_api_error( $result );
22551 }
22552
22553 /**
22554 * Checks if given API result is a non-empty and not an error object.
22555 *
22556 * @author Vova Feldman (@svovaf)
22557 * @since 1.2.1.5
22558 *
22559 * @param mixed $result
22560 * @param string|null $required_property Optional property we want to verify that is set.
22561 *
22562 * @return bool
22563 */
22564 function is_api_result_object( $result, $required_property = null ) {
22565 return FS_Api::is_api_result_object( $result, $required_property );
22566 }
22567
22568 /**
22569 * Checks if given API result is a non-empty entity object with non-empty ID.
22570 *
22571 * @author Vova Feldman (@svovaf)
22572 * @since 1.2.1.5
22573 *
22574 * @param mixed $result
22575 *
22576 * @return bool
22577 */
22578 private function is_api_result_entity( $result ) {
22579 return FS_Api::is_api_result_entity( $result );
22580 }
22581
22582 #endregion
22583
22584 /**
22585 * Make sure a given argument is an array of a specific type.
22586 *
22587 * @author Vova Feldman (@svovaf)
22588 * @since 1.2.1.5
22589 *
22590 * @param mixed $array
22591 * @param string $class
22592 *
22593 * @return bool
22594 */
22595 private function is_array_instanceof( $array, $class ) {
22596 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22597 }
22598
22599 /**
22600 * Start install ownership change.
22601 *
22602 * @author Vova Feldman (@svovaf)
22603 * @since 1.1.1
22604 * @uses FS_Api
22605 *
22606 * @param string $candidate_email
22607 * @param string $transfer_type
22608 *
22609 * @return bool Is ownership change successfully initiated.
22610 */
22611 private function init_change_owner( $candidate_email, $transfer_type ) {
22612 $this->_logger->entrance();
22613
22614 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22615 $install_ids = array();
22616
22617 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22618 $install = $install_info['install'];
22619
22620 if ( $this->_user->id != $install->user_id ) {
22621 // Skip add-on installs that are not owned by the parent product's install's owner.
22622 continue;
22623 }
22624
22625 $install_ids[ $slug ] = $install->id;
22626 }
22627
22628 $api = $this->get_api_site_scope();
22629 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22630 'email' => $candidate_email,
22631 'transfer_type' => $transfer_type,
22632 'install_ids' => implode( ',', array_values( $install_ids ) ),
22633 'after_confirm_url' => $this->_get_admin_page_url(
22634 'account',
22635 array( 'fs_action' => 'change_owner' )
22636 ),
22637 ) );
22638
22639 return ! $this->is_api_error( $result );
22640 }
22641
22642 /**
22643 * Handle install ownership change.
22644 *
22645 * @author Vova Feldman (@svovaf)
22646 * @since 1.1.1
22647 * @uses FS_Api
22648 *
22649 * @return bool Was ownership change successfully complete.
22650 */
22651 private function complete_change_owner() {
22652 $this->_logger->entrance();
22653
22654 $install_ids = fs_request_get( 'install_ids' );
22655
22656 if ( ! empty( $install_ids ) ) {
22657 $install_ids = explode( ',', $install_ids );
22658
22659 foreach ( $install_ids as $key => $install_id ) {
22660 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22661 unset( $install_ids[ $key ] );
22662 }
22663 }
22664 }
22665
22666 if ( ! is_array( $install_ids ) ) {
22667 $install_ids = array();
22668 }
22669
22670 $user = new FS_User();
22671 $user->id = fs_request_get( 'user_id' );
22672 $user->public_key = fs_request_get_raw( 'user_public_key' );
22673 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22674
22675 $prev_user = $this->_user;
22676 $this->_user = $user;
22677
22678 $result = $this->get_api_user_scope( true )->get(
22679 "/installs.json?install_ids=" . implode( ',', $install_ids )
22680 );
22681
22682 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22683
22684 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22685 $site_id_slug_map = array();
22686
22687 foreach ( $current_blog_sites as $slug => $site ) {
22688 $site_id_slug_map[ $site->id ] = $slug;
22689 }
22690
22691 foreach ( $result->installs as $install ) {
22692 $site = new FS_Site( $install );
22693
22694 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22695 continue;
22696 }
22697
22698 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22699
22700 if ( $this->_site->id == $site->id ) {
22701 $this->_site = $site;
22702 }
22703 }
22704 }
22705
22706 // Validate install's user and given user.
22707 if ( $user->id != $this->_site->user_id ) {
22708 $this->_user = $prev_user;
22709
22710 return false;
22711 }
22712
22713 $this->set_account_option( 'sites', $current_blog_sites, true );
22714
22715 // Fetch new user information.
22716 $user_result = $this->get_api_user_scope( true )->get();
22717 $user = new FS_User( $user_result );
22718 $this->_user = $user;
22719
22720 $this->_set_account( $user, $this->_site );
22721
22722 $remove_user = true;
22723 $all_modules_sites = self::get_all_modules_sites();
22724
22725 foreach ( $all_modules_sites as $sites_by_module_type ) {
22726 foreach ( $sites_by_module_type as $sites_by_slug ) {
22727 foreach ( $sites_by_slug as $site ) {
22728 if ( $prev_user->id == $site->user_id ) {
22729 $remove_user = false;
22730 break;
22731 }
22732 }
22733
22734 if ( ! $remove_user ) {
22735 break;
22736 }
22737 }
22738
22739 if ( ! $remove_user ) {
22740 break;
22741 }
22742 }
22743
22744 if ( $remove_user ) {
22745 $users = self::get_all_users();
22746
22747 if ( isset( $users[ $prev_user->id ] ) ) {
22748 unset( $users[ $prev_user->id ] );
22749 } else {
22750 // If the prev user wasn't found by the key, iterate over the users collection.
22751 foreach ( $users as $key => $user ) {
22752 if ( $user->id == $prev_user->id ) {
22753 unset( $users[ $key ] );
22754 break;
22755 }
22756 }
22757 }
22758
22759 $this->set_account_option( 'users', $users, true );
22760 }
22761
22762 return true;
22763 }
22764
22765 /**
22766 * Completes ownership change by license.
22767 *
22768 * @author Leo Fajardo (@leorw)
22769 * @since 2.3.2
22770 *
22771 * @param number $user_id
22772 * @param array[string]number $install_ids_by_slug_map
22773 *
22774 */
22775 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22776 $this->_logger->entrance();
22777
22778 $this->sync_user_by_current_install( $user_id );
22779
22780 $result = $this->get_api_user_scope( true )->get(
22781 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22782 );
22783
22784 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22785 $sites = self::get_all_sites( $this->get_module_type() );
22786 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22787
22788 foreach ( $result->installs as $install ) {
22789 $site = new FS_Site( $install );
22790
22791 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22792 }
22793
22794 $this->set_account_option( 'sites', $sites, true );
22795 }
22796 }
22797
22798 /**
22799 * Handle user name update.
22800 *
22801 * @author Vova Feldman (@svovaf)
22802 * @since 1.0.9
22803 * @uses FS_Api
22804 *
22805 * @return object
22806 */
22807 private function update_user_name() {
22808 $this->_logger->entrance();
22809 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22810
22811 $api = $this->get_api_user_scope();
22812 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22813 'name' => $name,
22814 ) );
22815
22816 if ( ! isset( $user->error ) ) {
22817 $this->_user->first = $user->first;
22818 $this->_user->last = $user->last;
22819 $this->_store_user();
22820 } else {
22821 // handle different error cases.
22822
22823 }
22824
22825 return $user;
22826 }
22827
22828 /**
22829 * Verify user email.
22830 *
22831 * @author Vova Feldman (@svovaf)
22832 * @since 1.0.3
22833 * @uses FS_Api
22834 */
22835 private function verify_email() {
22836 $this->_handle_account_user_sync();
22837
22838 if ( $this->_user->is_verified() ) {
22839 return;
22840 }
22841
22842 $api = $this->get_api_site_scope();
22843 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22844 'after_email_confirm_url' => $this->_get_admin_page_url(
22845 'account',
22846 array( 'fs_action' => 'sync_user' )
22847 )
22848 ) );
22849
22850 if ( ! isset( $result->error ) ) {
22851 $this->_admin_notices->add( sprintf(
22852 $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' ),
22853 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22854 ) );
22855 } else {
22856 // handle different error cases.
22857
22858 }
22859 }
22860
22861 /**
22862 * @author Vova Feldman (@svovaf)
22863 * @since 1.1.2
22864 *
22865 * @param array $params
22866 * @param bool|null $network
22867 *
22868 * @return string
22869 */
22870 function get_activation_url( $params = array(), $network = null ) {
22871 if ( $this->is_addon() && $this->has_free_plan() ) {
22872 /**
22873 * @author Vova Feldman (@svovaf)
22874 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22875 */
22876 return $this->get_parent_instance()->get_activation_url( $params );
22877 }
22878
22879 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22880 }
22881
22882 /**
22883 * @author Vova Feldman (@svovaf)
22884 * @since 1.2.1.5
22885 *
22886 * @param array $params
22887 *
22888 * @return string
22889 */
22890 function get_reconnect_url( $params = array() ) {
22891 $params['fs_action'] = 'reset_anonymous_mode';
22892 $params['fs_unique_affix'] = $this->get_unique_affix();
22893
22894 return $this->get_activation_url( $params );
22895 }
22896
22897 /**
22898 * Get the URL of the page that should be loaded after the user connect
22899 * or skip in the opt-in screen.
22900 *
22901 * @author Vova Feldman (@svovaf)
22902 * @since 1.1.3
22903 *
22904 * @param string $filter Filter name.
22905 * @param array $params Since 1.2.2.7
22906 * @param bool|null $network
22907 *
22908 * @return string
22909 */
22910 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22911 if ( $this->show_opt_in_on_themes_page() &&
22912 ( fs_request_has( 'pending_activation' ) ||
22913 // For cases when the first time path is set, even though it's a WP.org theme.
22914 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22915 ) {
22916 $first_time_path = '';
22917 } else {
22918 $first_time_path = $this->_menu->get_first_time_path(
22919 fs_is_network_admin() && $this->_is_network_active
22920 );
22921 }
22922
22923 if ( $this->_is_network_active &&
22924 fs_is_network_admin() &&
22925 ! $this->_menu->has_network_menu() &&
22926 $this->is_network_registered()
22927 ) {
22928 $target_url = $this->get_account_url();
22929 } else {
22930 // Default plugin's page.
22931 $target_url = $this->_get_admin_page_url( '', array(), $network );
22932 }
22933
22934 return add_query_arg( $params, $this->apply_filters(
22935 $filter,
22936 empty( $first_time_path ) ?
22937 $target_url :
22938 $first_time_path
22939 ) );
22940 }
22941
22942 /**
22943 * Handle account page updates / edits / actions.
22944 *
22945 * @author Vova Feldman (@svovaf)
22946 * @since 1.0.2
22947 *
22948 */
22949 private function _handle_account_edits() {
22950 if ( ! $this->is_user_admin() ) {
22951 return;
22952 }
22953
22954 $action = fs_get_action();
22955
22956 if ( empty( $action ) ) {
22957 return;
22958 }
22959
22960 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22961 $install_id = fs_request_get( 'install_id', '' );
22962
22963 // Alias.
22964 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22965
22966 $is_network_action = $this->is_network_level_action();
22967 $blog_id = $this->is_network_level_site_specific_action();
22968 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22969
22970 if ( is_numeric( $blog_id ) ) {
22971 $this->switch_to_blog( $blog_id );
22972 } else {
22973 $blog_id = '';
22974 }
22975
22976 switch ( $action ) {
22977 case 'opt_in':
22978 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22979
22980 if ( $is_parent_plugin_action ) {
22981 if ( $is_network_action && ! empty( $blog_id ) ) {
22982 if ( ! $this->is_registered() ) {
22983 $this->install_with_user(
22984 $this->get_network_user(),
22985 false,
22986 false,
22987 false,
22988 false
22989 );
22990
22991 $this->_admin_notices->add(
22992 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22993 $this->get_text_inline( 'Awesome', 'awesome' )
22994 );
22995 }
22996 }
22997 }
22998 break;
22999
23000 case 'toggle_tracking':
23001 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23002
23003 if ( $is_parent_plugin_action ) {
23004 if ( $is_network_action && ! empty( $blog_id ) ) {
23005 if ( $this->is_registered( true ) ) {
23006 if ( $this->is_tracking_prohibited( $blog_id ) ) {
23007 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
23008 $this->_admin_notices->add(
23009 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>" ),
23010 $this->get_text_inline( 'Thank you!', 'thank-you' )
23011 );
23012 }
23013 } else {
23014 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
23015 $install = $this->get_install_by_blog_id( $blog_id );
23016
23017 $this->_admin_notices->add(
23018 sprintf(
23019 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
23020 self::get_unfiltered_site_url( $blog_id, true ),
23021 "<b>{$this->get_plugin_title()}</b>"
23022 )
23023 );
23024 }
23025 }
23026 }
23027 }
23028 }
23029
23030 break;
23031
23032 case 'delete_account':
23033 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23034
23035 $is_network_deletion = $is_network_action && empty( $blog_id );
23036
23037 if ( $is_parent_plugin_action ) {
23038 // Delete add-on installs if have any.
23039 $installed_addons = $this->get_installed_addons();
23040 foreach ( $installed_addons as $fs_addon ) {
23041 if ( $is_network_deletion ) {
23042 $fs_addon->delete_network_account_event();
23043 } else {
23044 $fs_addon->delete_account_event();
23045 }
23046 }
23047
23048 if ( $is_network_deletion ) {
23049 $this->delete_network_account_event();
23050 } else {
23051 $this->delete_account_event();
23052 }
23053
23054 // Clear user and site.
23055 $this->_site = null;
23056 $this->_user = null;
23057
23058 $this->maybe_set_slug_and_network_menu_exists_flag();
23059
23060 fs_redirect( $this->get_activation_url() );
23061 } else {
23062 if ( $this->is_addon_activated( $plugin_id ) ) {
23063 $fs_addon = self::get_instance_by_id( $plugin_id );
23064
23065 if ( $is_network_deletion ) {
23066 $fs_addon->delete_network_account_event();
23067 } else {
23068 $fs_addon->delete_account_event();
23069 }
23070
23071 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23072 }
23073 }
23074
23075 return;
23076
23077 case 'downgrade_account':
23078 if ( is_numeric( $blog_id ) ) {
23079 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23080 } else {
23081 check_admin_referer( $action );
23082 }
23083
23084 $switch_to_network_install_blog_after_cancellation = (
23085 is_numeric( $blog_id ) &&
23086 $plugin_id == $this->get_id() &&
23087 ! $this->is_trial()
23088 );
23089
23090 $result = $this->cancel_subscription_or_trial( $plugin_id );
23091 if ( $this->is_api_error( $result ) ) {
23092 $this->_admin_notices->add(
23093 $result->error->message,
23094 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23095 'error'
23096 );
23097 }
23098
23099 if ( $switch_to_network_install_blog_after_cancellation ) {
23100 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23101 }
23102
23103 return;
23104
23105 case 'activate_license':
23106 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23107
23108 $fs = $this;
23109 if ( $plugin_id != $this->get_id() ) {
23110 $fs = $this->is_addon_activated( $plugin_id ) ?
23111 self::get_instance_by_id( $plugin_id ) :
23112 null;
23113 }
23114
23115 if ( is_object( $fs ) ) {
23116 $fs->_activate_license();
23117
23118 /**
23119 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23120 *
23121 * @author Leo Fajardo (@leorw)
23122 * @since 2.4.0
23123 */
23124 unset( $_REQUEST['plugin_id'] );
23125
23126 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23127 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23128 }
23129 }
23130
23131 return;
23132
23133 case 'deactivate_license':
23134 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23135
23136 if ( $plugin_id == $this->get_id() ) {
23137 $this->_deactivate_license();
23138
23139 if ( $this->is_only_premium() ) {
23140 // Clear user and site.
23141 $this->_site = null;
23142 $this->_user = null;
23143
23144 if ( ! $is_network_action ) {
23145 fs_redirect( $this->get_activation_url() );
23146 } else if ( is_numeric( $blog_id ) ) {
23147 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23148 }
23149 }
23150 } else {
23151 if ( $this->is_addon_activated( $plugin_id ) ) {
23152 $fs_addon = self::get_instance_by_id( $plugin_id );
23153 $fs_addon->_deactivate_license();
23154 }
23155 }
23156
23157 return;
23158
23159 case 'check_updates':
23160 check_admin_referer( $action );
23161 $this->check_updates();
23162
23163 return;
23164
23165 case 'change_owner':
23166 $state = fs_request_get( 'state', 'init' );
23167 switch ( $state ) {
23168 case 'init':
23169 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23170 check_admin_referer( 'change_owner' );
23171
23172 $candidate_email = fs_request_get( 'candidate_email' );
23173 $transfer_type = fs_request_get( 'transfer_type' );
23174
23175 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23176 if ( 'transfer' === $transfer_type ) {
23177 $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>' ) );
23178 } else {
23179 $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>' ) );
23180 }
23181 }
23182 break;
23183 case 'owner_confirmed':
23184 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23185 $candidate_email = fs_request_get( 'candidate_email', '' );
23186
23187 if ( ! is_email($candidate_email ) ) {
23188 return;
23189 }
23190
23191 $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>' ) );
23192 break;
23193 case 'candidate_confirmed':
23194 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23195 if ( $this->complete_change_owner() ) {
23196 $this->_admin_notices->add_sticky(
23197 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23198 'ownership_changed',
23199 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23200 );
23201 } else {
23202 // @todo Handle failed ownership change message.
23203 }
23204 break;
23205 }
23206
23207 return;
23208
23209 case 'update_user_name':
23210 check_admin_referer( 'update_user_name' );
23211
23212 $result = $this->update_user_name();
23213
23214 if ( isset( $result->error ) ) {
23215 $this->_admin_notices->add(
23216 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23217 $oops_text,
23218 'error'
23219 );
23220 } else {
23221 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23222 }
23223
23224 return;
23225
23226 #region Actions that might be called from external links (e.g. email)
23227
23228 /**
23229 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23230 */
23231
23232 case 'cancel_trial':
23233 $result = $this->cancel_subscription_or_trial( $plugin_id );
23234 if ( $this->is_api_error( $result ) ) {
23235 $this->_admin_notices->add(
23236 $result->error->message,
23237 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23238 'error'
23239 );
23240 }
23241
23242 return;
23243
23244 case 'verify_email':
23245 $this->verify_email();
23246
23247 return;
23248
23249 case 'sync_user':
23250 $this->_handle_account_user_sync();
23251
23252 return;
23253
23254 case $this->get_unique_affix() . '_sync_license':
23255 $this->_sync_license();
23256
23257 return;
23258
23259 case 'download_latest':
23260 $this->download_latest_directly( $plugin_id );
23261
23262 return;
23263
23264 #endregion
23265 }
23266
23267 if ( WP_FS__IS_POST_REQUEST ) {
23268 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23269 foreach ( $properties as $p ) {
23270 if ( 'update_' . $p === $action ) {
23271 check_admin_referer( $action );
23272
23273 $this->_logger->log( $action );
23274
23275 $site_property = substr( $p, strlen( 'site_' ) );
23276 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23277 $this->get_site()->{$site_property} = $site_property_value;
23278
23279 // Store account after modification.
23280 $this->_store_site();
23281
23282 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23283
23284 $this->_admin_notices->add( sprintf(
23285 /* translators: %s: User's account property (e.g. email address, name) */
23286 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23287 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23288 ) );
23289
23290 return;
23291 }
23292 }
23293 }
23294 }
23295
23296 /**
23297 * Adds CSS classes for the body tag in the admin.
23298 *
23299 * @param string $classes Space-separated string of class names.
23300 *
23301 * @return string $classes FS Admin body tag class names.
23302 */
23303 public function fs_addons_body_class( $classes ) {
23304 $classes .= ' plugins-php';
23305 return $classes;
23306 }
23307
23308 /**
23309 * Account page resources load.
23310 *
23311 * @author Vova Feldman (@svovaf)
23312 * @since 1.0.6
23313 */
23314 function _account_page_load() {
23315 $this->_logger->entrance();
23316
23317 $this->_logger->info( var_export( $_REQUEST, true ) );
23318
23319 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23320
23321 if ( $this->has_addons() ) {
23322 wp_enqueue_script( 'plugin-install' );
23323 add_thickbox();
23324 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23325 }
23326
23327 if ( $this->has_paid_plan() &&
23328 ! $this->has_any_license() &&
23329 ! $this->is_sync_executed() &&
23330 $this->is_tracking_allowed()
23331 ) {
23332 /**
23333 * If no licenses found and no sync job was executed during the last 24 hours,
23334 * just execute the sync job right away (blocking execution).
23335 *
23336 * @since 1.1.7.3
23337 */
23338 $this->run_manual_sync();
23339 }
23340
23341 $this->_handle_account_edits();
23342
23343 if (
23344 is_object( $this->_license ) &&
23345 $this->_license->user_id == $this->_user->id &&
23346 ! $this->is_whitelabeled( true )
23347 ) {
23348 $this->_admin_notices->add(
23349 sprintf(
23350 $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' ),
23351 sprintf(
23352 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23353 $this->get_text_inline( 'Click here', 'click-here' )
23354 )
23355 ),
23356 '',
23357 'success',
23358 false,
23359 'license_not_whitelabeled'
23360 );
23361 }
23362
23363 $this->do_action( 'account_page_load_before_departure' );
23364 }
23365
23366 /**
23367 * Renders the "Affiliation" page.
23368 *
23369 * @author Leo Fajardo (@leorw)
23370 * @since 1.2.3
23371 */
23372 function _affiliation_page_render() {
23373 $this->_logger->entrance();
23374
23375 $this->fetch_affiliate_and_terms();
23376
23377 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23378
23379 $is_bundle_context = $this->has_bundle_context();
23380
23381 $plugin_title = $this->get_plugin_title();
23382
23383 if ( $is_bundle_context ) {
23384 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23385
23386 // Add the suffix "Bundle" only if the word is not present in the title itself.
23387 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23388 $plugin_title = $this->apply_filters(
23389 'formatted_bundle_title',
23390 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23391 );
23392 }
23393 }
23394
23395 $vars = array(
23396 'id' => $this->_module_id,
23397 'plugin_title' => $plugin_title,
23398 );
23399 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23400 }
23401
23402
23403 /**
23404 * Render account page.
23405 *
23406 * @author Vova Feldman (@svovaf)
23407 * @since 1.0.0
23408 */
23409 function _account_page_render() {
23410 $this->_logger->entrance();
23411
23412 $template = 'account.php';
23413 $vars = array( 'id' => $this->_module_id );
23414
23415 /**
23416 * Added filter to the template to allow developers wrapping the template
23417 * in custom HTML (e.g. within a wizard/tabs).
23418 *
23419 * @author Vova Feldman (@svovaf)
23420 * @since 1.2.1.6
23421 */
23422 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23423 }
23424
23425 /**
23426 * Render account connect page.
23427 *
23428 * @author Vova Feldman (@svovaf)
23429 * @since 1.0.7
23430 */
23431 function _connect_page_render() {
23432 $this->_logger->entrance();
23433
23434 $vars = array( 'id' => $this->_module_id );
23435
23436 /**
23437 * Added filter to the template to allow developers wrapping the template
23438 * in custom HTML (e.g. within a wizard/tabs).
23439 *
23440 * @author Vova Feldman (@svovaf)
23441 * @since 1.2.1.6
23442 */
23443 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23444 }
23445
23446 /**
23447 * Load required resources before add-ons page render.
23448 *
23449 * @author Vova Feldman (@svovaf)
23450 * @since 1.0.6
23451 */
23452 function _addons_page_load() {
23453 $this->_logger->entrance();
23454
23455 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23456
23457 wp_enqueue_script( 'plugin-install' );
23458 add_thickbox();
23459 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23460
23461 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23462 $this->_admin_notices->add(
23463 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>' ),
23464 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23465 'update-nag'
23466 );
23467 }
23468 }
23469
23470 /**
23471 * Render add-ons page.
23472 *
23473 * @author Vova Feldman (@svovaf)
23474 * @since 1.0.6
23475 */
23476 function _addons_page_render() {
23477 $this->_logger->entrance();
23478
23479 $vars = array( 'id' => $this->_module_id );
23480
23481 /**
23482 * Added filter to the template to allow developers wrapping the template
23483 * in custom HTML (e.g. within a wizard/tabs).
23484 *
23485 * @author Vova Feldman (@svovaf)
23486 * @since 1.2.1.6
23487 */
23488 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23489 }
23490
23491 /* Pricing & Upgrade
23492 ------------------------------------------------------------------------------------------------------------------*/
23493 /**
23494 * Render pricing page.
23495 *
23496 * @author Vova Feldman (@svovaf)
23497 * @since 1.0.0
23498 */
23499 function _pricing_page_render() {
23500 $this->_logger->entrance();
23501
23502 $vars = array( 'id' => $this->_module_id );
23503
23504 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23505 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23506 } else {
23507 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23508 }
23509 }
23510
23511 /**
23512 * @author Leo Fajardo (@leorw)
23513 * @since 2.3.1
23514 */
23515 function _maybe_add_pricing_ajax_handler() {
23516 if ( ! $this->should_use_external_pricing() ) {
23517 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23518 }
23519 }
23520
23521 /**
23522 * @author Leo Fajardo (@leorw)
23523 * @since 2.3.1
23524 */
23525 function _fs_pricing_ajax_action_handler() {
23526 $this->check_ajax_referer( 'pricing_ajax_action' );
23527
23528 $result = null;
23529 $pricing_action = fs_request_get( 'pricing_action' );
23530
23531 switch ( $pricing_action ) {
23532 case 'fetch_pricing_data':
23533 $params = array(
23534 'is_enriched' => true,
23535 'trial' => fs_request_get_bool( 'trial' ),
23536 'sandbox' => fs_request_get_raw( 'sandbox' ),
23537 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23538 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23539 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23540 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23541 );
23542
23543 $bundle_id = $this->get_bundle_id();
23544 $bundle_public_key = $this->get_bundle_public_key();
23545
23546 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23547
23548 if ( ! $has_bundle_context ) {
23549 $api = $this->get_api_plugin_scope();
23550 } else {
23551 $api = FS_Api::instance(
23552 $bundle_id,
23553 'plugin',
23554 $bundle_id,
23555 $bundle_public_key,
23556 ! $this->is_live(),
23557 false,
23558 $this->get_sdk_version()
23559 );
23560
23561 $params['plugin_id'] = $this->get_id();
23562 $params['plugin_public_key'] = $this->get_public_key();
23563 }
23564
23565 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23566 break;
23567 case 'start_trial':
23568 $result = $this->opt_in(
23569 false,
23570 false,
23571 false,
23572 false,
23573 false,
23574 fs_request_get( 'plan_id' )
23575 );
23576 }
23577
23578 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23579 $this->_logger->api_error( $result );
23580
23581 self::shoot_ajax_failure(
23582 isset( $result->error ) ?
23583 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23584 var_export( $result, true )
23585 );
23586 }
23587
23588 $this->shoot_ajax_success( $result );
23589 }
23590
23591 #----------------------------------------------------------------------------------
23592 #region Contact Us
23593 #----------------------------------------------------------------------------------
23594
23595 /**
23596 * Render contact-us page.
23597 *
23598 * @author Vova Feldman (@svovaf)
23599 * @since 1.0.3
23600 */
23601 function _contact_page_render() {
23602 $this->_logger->entrance();
23603
23604 $vars = array( 'id' => $this->_module_id );
23605
23606 /**
23607 * Added filter to the template to allow developers wrapping the template
23608 * in custom HTML (e.g. within a wizard/tabs).
23609 *
23610 * @author Vova Feldman (@svovaf)
23611 * @since 2.1.3
23612 */
23613 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23614 }
23615
23616 #endregion ------------------------------------------------------------------------
23617
23618 /**
23619 * Hide all admin notices to prevent distractions.
23620 *
23621 * @author Vova Feldman (@svovaf)
23622 * @since 1.0.3
23623 *
23624 * @uses remove_all_actions()
23625 */
23626 private static function _hide_admin_notices() {
23627 remove_all_actions( 'admin_notices' );
23628 remove_all_actions( 'network_admin_notices' );
23629 remove_all_actions( 'all_admin_notices' );
23630 remove_all_actions( 'user_admin_notices' );
23631 }
23632
23633 static function _clean_admin_content_section_hook() {
23634 $hide_admin_notices = true;
23635
23636 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23637 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23638
23639 $hide_admin_notices = false;
23640 }
23641
23642 if ( $hide_admin_notices ) {
23643 self::_hide_admin_notices();
23644 }
23645
23646 // Hide footer.
23647 echo '<style>#wpfooter { display: none !important; }</style>';
23648 }
23649
23650 /**
23651 * Attach to admin_head hook to hide all admin notices.
23652 *
23653 * @author Vova Feldman (@svovaf)
23654 * @since 1.0.3
23655 */
23656 static function _clean_admin_content_section() {
23657 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23658 }
23659
23660 /* CSS & JavaScript
23661 ------------------------------------------------------------------------------------------------------------------*/
23662 /* function _enqueue_script($handle, $src) {
23663 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23664
23665 $this->_logger->entrance( 'script = ' . $url );
23666
23667 wp_enqueue_script( $handle, $url );
23668 }*/
23669
23670 /* SDK
23671 ------------------------------------------------------------------------------------------------------------------*/
23672 private $_user_api;
23673
23674 /**
23675 *
23676 * @author Vova Feldman (@svovaf)
23677 * @since 1.0.2
23678 *
23679 * @param bool $flush
23680 *
23681 * @return FS_Api
23682 */
23683 function get_api_user_scope( $flush = false ) {
23684 if ( ! isset( $this->_user_api ) || $flush ) {
23685 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23686 }
23687
23688 return $this->_user_api;
23689 }
23690
23691 /**
23692 * @author Vova Feldman (@svovaf)
23693 * @since 2.0.0
23694 *
23695 * @param \FS_User $user
23696 *
23697 * @return \FS_Api
23698 */
23699 private function get_api_user_scope_by_user( FS_User $user ) {
23700 return FS_Api::instance(
23701 $this->_module_id,
23702 'user',
23703 $user->id,
23704 $user->public_key,
23705 ! $this->is_live(),
23706 $user->secret_key,
23707 $this->get_sdk_version()
23708 );
23709 }
23710
23711 /**
23712 *
23713 * @author Leo Fajardo (@leorw)
23714 * @since 2.0.0
23715 *
23716 * @param bool $flush
23717 *
23718 * @return FS_Api
23719 */
23720 private function get_current_or_network_user_api_scope( $flush = false ) {
23721 if ( ! $this->_is_network_active ||
23722 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23723 ) {
23724 return $this->get_api_user_scope( $flush );
23725 }
23726
23727 $user = $this->get_current_or_network_user();
23728
23729 $this->_user_api = FS_Api::instance(
23730 $this->_module_id,
23731 'user',
23732 $user->id,
23733 $user->public_key,
23734 ! $this->is_live(),
23735 $user->secret_key,
23736 $this->get_sdk_version()
23737 );
23738
23739 return $this->_user_api;
23740 }
23741
23742 private $_site_api;
23743
23744 /**
23745 *
23746 * @author Vova Feldman (@svovaf)
23747 * @since 1.0.2
23748 *
23749 * @param bool $flush
23750 *
23751 * @return FS_Api
23752 */
23753 private function get_api_site_scope( $flush = false ) {
23754 if ( ! isset( $this->_site_api ) || $flush ) {
23755 $this->_site_api = FS_Api::instance(
23756 $this->_module_id,
23757 'install',
23758 $this->_site->id,
23759 $this->_site->public_key,
23760 ! $this->is_live(),
23761 $this->_site->secret_key,
23762 $this->get_sdk_version(),
23763 self::get_unfiltered_site_url()
23764 );
23765 }
23766
23767 return $this->_site_api;
23768 }
23769
23770 /**
23771 * @author Leo Fajardo (@leorw)
23772 * @since 2.5.0
23773 *
23774 * @param string $path
23775 * @param string $method
23776 * @param array $params
23777 * @param bool $flush_instance
23778 *
23779 * @return array|mixed|string|void
23780 * @throws Freemius_Exception
23781 */
23782 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23783 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23784
23785 /**
23786 * Checks if the local install's URL is different from the remote install's URL, update the local install if necessary, and then run the clone handler if the install's URL is different from the URL of the site.
23787 *
23788 * @author Leo Fajardo (@leorw)
23789 * @since 2.5.0
23790 */
23791 if (
23792 $this->is_registered() &&
23793 FS_Api::is_api_result_entity( $result ) &&
23794 isset( $result->url )
23795 ) {
23796 $stored_local_url = trailingslashit( $this->_site->url );
23797 $stored_remote_url = trailingslashit( $result->url );
23798
23799 if ( $stored_local_url !== $stored_remote_url ) {
23800 $this->_site->url = $result->url;
23801 $this->_store_site();
23802 }
23803
23804 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23805 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23806 }
23807 }
23808
23809 return $result;
23810 }
23811
23812 private $_plugin_api;
23813
23814 /**
23815 * Get plugin public API scope.
23816 *
23817 * @author Vova Feldman (@svovaf)
23818 * @since 1.0.7
23819 *
23820 * @return FS_Api
23821 */
23822 function get_api_plugin_scope() {
23823 if ( ! isset( $this->_plugin_api ) ) {
23824 $this->_plugin_api = FS_Api::instance(
23825 $this->_module_id,
23826 'plugin',
23827 $this->_plugin->id,
23828 $this->_plugin->public_key,
23829 ! $this->is_live(),
23830 false,
23831 $this->get_sdk_version()
23832 );
23833 }
23834
23835 return $this->_plugin_api;
23836 }
23837
23838 /**
23839 * Get bundle public API scope.
23840 *
23841 * @author Vova Feldman (@svovaf)
23842 * @since 2.3.1
23843 *
23844 * @return FS_Api
23845 */
23846 function get_api_bundle_scope() {
23847 return FS_Api::instance(
23848 $this->get_bundle_id(),
23849 'plugin',
23850 $this->get_bundle_id(),
23851 $this->get_bundle_public_key(),
23852 ! $this->is_live(),
23853 false,
23854 $this->get_sdk_version()
23855 );
23856 }
23857
23858 /**
23859 * Get site API scope object (fallback to public plugin scope when not registered).
23860 *
23861 * @author Vova Feldman (@svovaf)
23862 * @since 1.0.7
23863 *
23864 * @return FS_Api
23865 */
23866 function get_api_site_or_plugin_scope() {
23867 return $this->is_registered() ?
23868 $this->get_api_site_scope() :
23869 $this->get_api_plugin_scope();
23870 }
23871
23872 /**
23873 * @author Leo Fajardo (@leorw)
23874 * @since 2.2.3.1
23875 *
23876 * @param object $result
23877 */
23878 private function maybe_modify_api_curl_error_message( $result ) {
23879 if (
23880 'cUrlMissing' !== $result->error->type &&
23881 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23882 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23883 ) {
23884 return;
23885 }
23886
23887 $result->error->message = $this->esc_html_inline( 'We use PHP cURL library for the API calls, which is a very common library and usually installed and activated out of the box. Unfortunately, cURL is not activated (or disabled) on your server.', 'curl-missing-message' ) .
23888 ' ' .
23889 $this->esc_html_inline(
23890 sprintf(
23891 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23892 implode(
23893 ', ',
23894 $this->apply_filters( 'api_domains', array(
23895 'api.freemius.com',
23896 'wp.freemius.com'
23897 ) )
23898 )
23899 ),
23900 'connectivity-whitelist'
23901 ) .
23902 ' ' .
23903 sprintf(
23904 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23905 $this->get_module_type()
23906 );
23907 }
23908
23909 /**
23910 * Show trial promotional notice (if any trial exist).
23911 *
23912 * @author Vova Feldman (@svovaf)
23913 * @since 1.0.9
23914 *
23915 * @param FS_Plugin_Plan[] $plans
23916 */
23917 function _check_for_trial_plans( $plans ) {
23918 /**
23919 * For some reason core's do_action() flattens arrays when it has a single object item. Therefore, we need to restructure the array as expected.
23920 *
23921 * @author Vova Feldman (@svovaf)
23922 * @since 2.1.2
23923 */
23924 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23925 $plans = array( $plans );
23926 }
23927
23928 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23929 $plans = array();
23930 }
23931
23932 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23933 }
23934
23935 /**
23936 * During trial promotion the "upgrade" submenu item turns to
23937 * "start trial" to encourage the trial. Since we want to keep
23938 * the same menu item handler and there's no robust way to
23939 * add new arguments to the menu item link's querystring,
23940 * use JavaScript to find the menu item and update the href of
23941 * the link.
23942 *
23943 * @author Vova Feldman (@svovaf)
23944 * @since 1.2.1.5
23945 */
23946 function _fix_start_trial_menu_item_url() {
23947 $template_args = array( 'id' => $this->_module_id );
23948 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23949 }
23950
23951 /**
23952 * Check if module is currently in a trial promotion mode.
23953 *
23954 * @author Vova Feldman (@svovaf)
23955 * @since 1.2.2.7
23956 *
23957 * @return bool
23958 */
23959 function is_in_trial_promotion() {
23960 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23961 }
23962
23963 /**
23964 * Show trial promotional notice (if any trial exist).
23965 *
23966 * @author Vova Feldman (@svovaf)
23967 * @since 1.0.9
23968 *
23969 * @return bool If trial notice added.
23970 */
23971 function _add_trial_notice() {
23972 if ( ! $this->is_user_admin() ) {
23973 return false;
23974 }
23975
23976 if ( ! $this->is_user_in_admin() ) {
23977 return false;
23978 }
23979
23980 if ( $this->_is_network_active ) {
23981 if ( fs_is_network_admin() ) {
23982 // Network level trial is disabled at the moment.
23983 return false;
23984 }
23985
23986 if ( ! $this->is_delegated_connection() ) {
23987 // Only delegated sites should support trials.
23988 return false;
23989 }
23990 }
23991
23992 // Check if trial message is already shown.
23993 if ( $this->is_in_trial_promotion() ) {
23994 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23995
23996 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23997
23998 return false;
23999 }
24000
24001 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
24002 // Don't show trial if running the premium code, unless running in DEV mode.
24003 return false;
24004 }
24005
24006 if ( ! $this->has_trial_plan() ) {
24007 // No plans with trial.
24008 return false;
24009 }
24010
24011 if ( ! $this->apply_filters( 'show_trial', true ) ) {
24012 // Developer explicitly asked not to show the trial promo.
24013 return false;
24014 }
24015
24016 if ( $this->is_registered() ) {
24017 // Check if trial already utilized.
24018 if ( $this->_site->is_trial_utilized() ) {
24019 return false;
24020 }
24021
24022 if ( $this->is_paying_or_trial() ) {
24023 // Don't show trial if paying or already in trial.
24024 return false;
24025 }
24026 }
24027
24028 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24029 // If not yet opted-in/skipped, or pending activation, don't show trial.
24030 return false;
24031 }
24032
24033 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24034 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24035
24036 // Show promotion if never shown before and 24 hours after initial activation with FS.
24037 if ( ! $was_promotion_shown_before &&
24038 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24039 ) {
24040 return false;
24041 }
24042
24043 // OR if promotion was shown before, try showing it every 30 days.
24044 if ( $was_promotion_shown_before &&
24045 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24046 ) {
24047 return false;
24048 }
24049
24050 $trial_period = $this->_trial_days;
24051 $require_payment = $this->_is_trial_require_payment;
24052 $trial_url = $this->get_trial_url();
24053 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24054
24055 if ( $this->is_registered() ) {
24056 // If opted-in, override trial with up to date data from API.
24057 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
24058 $trial_plans_count = count( $trial_plans );
24059
24060 if ( 0 === $trial_plans_count ) {
24061 // If there's no plans with a trial just exit.
24062 return false;
24063 }
24064
24065 /**
24066 * @var FS_Plugin_Plan $paid_plan
24067 */
24068 $paid_plan = $trial_plans[0];
24069 $require_payment = $paid_plan->is_require_subscription;
24070 $trial_period = $paid_plan->trial_period;
24071
24072 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24073
24074 if ( $total_paid_plans !== $trial_plans_count ) {
24075 // Not all paid plans have a trial - generate a string of those that have it.
24076 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24077 $plans_string .= sprintf(
24078 ' <a href="%s">%s</a>',
24079 $trial_url,
24080 $trial_plans[ $i ]->title
24081 );
24082
24083 if ( $i < $trial_plans_count - 2 ) {
24084 $plans_string .= ', ';
24085 } else if ( $i == $trial_plans_count - 2 ) {
24086 $plans_string .= ' and ';
24087 }
24088 }
24089 }
24090 }
24091
24092 $message = sprintf(
24093 $this->get_text_x_inline( 'Hey', 'exclamation', 'hey' ) . '! ' . $this->get_text_inline( 'How do you like %s so far? Test all our %s premium features with a %d-day free trial.', 'trial-x-promotion-message' ),
24094 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24095 $plans_string,
24096 $trial_period
24097 );
24098
24099 // "No Credit-Card Required" or "No Commitment for N Days".
24100 $cc_string = $require_payment ?
24101 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24102 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24103
24104
24105 // Start trial button.
24106 $button = ' ' . sprintf(
24107 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24108 $trial_url,
24109 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24110 );
24111
24112 $this->_admin_notices->add_sticky(
24113 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
24114 'trial_promotion',
24115 '',
24116 'promotion'
24117 );
24118
24119 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24120
24121 return true;
24122 }
24123
24124 /**
24125 * Lets users/customers know that the product has an affiliate program.
24126 *
24127 * @author Leo Fajardo (@leorw)
24128 * @since 1.2.2.11
24129 *
24130 * @return bool Returns true if the notice has been added.
24131 */
24132 function _add_affiliate_program_notice() {
24133 if ( ! $this->is_user_admin() ) {
24134 return false;
24135 }
24136
24137 if ( ! $this->is_user_in_admin() ) {
24138 return false;
24139 }
24140
24141 // Check if the notice is already shown.
24142 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24143 return false;
24144 }
24145
24146 if (
24147 // Product has no affiliate program.
24148 ! $this->has_affiliate_program() ||
24149 // User has applied for an affiliate account.
24150 ! empty( $this->_storage->affiliate_application_data )
24151 ) {
24152 return false;
24153 }
24154
24155 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24156 // Developer explicitly asked not to show the notice about the affiliate program.
24157 return false;
24158 }
24159
24160 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24161 // If not yet opted in/skipped, or pending activation, don't show the notice.
24162 return false;
24163 }
24164
24165 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24166 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24167
24168 /**
24169 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24170 * activation with FS.
24171 */
24172 if ( $was_notice_shown_before ||
24173 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24174 ) {
24175 return false;
24176 }
24177
24178 if ( ! $this->is_paying() &&
24179 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24180 ) {
24181 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24182 return false;
24183 }
24184
24185 $message = sprintf(
24186 $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' ),
24187 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24188 $this->get_module_label( true )
24189 );
24190
24191 // HTML code for the "Learn more..." button.
24192 $button = ' ' . sprintf(
24193 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24194 $this->_get_admin_page_url( 'affiliation' ),
24195 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24196 );
24197
24198 $this->_admin_notices->add_sticky(
24199 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24200 'affiliate_program',
24201 '',
24202 'promotion'
24203 );
24204
24205 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24206
24207 return true;
24208 }
24209
24210 /**
24211 * @author Vova Feldman (@svovaf)
24212 * @since 1.2.1.5
24213 */
24214 function _enqueue_common_css() {
24215 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24216 // Add basic CSS for admin-notices and menu-item colors.
24217 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24218 }
24219 }
24220
24221 /**
24222 * @author Leo Fajardo (@leorw)
24223 * @since 1.2.2
24224 */
24225 function _show_theme_activation_optin_dialog() {
24226 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24227
24228 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24229 }
24230
24231 /**
24232 * @author Leo Fajardo (@leorw)
24233 * @since 1.2.2
24234 */
24235 function _add_fs_theme_activation_dialog() {
24236 global $pagenow;
24237
24238 if ( 'themes.php' !== $pagenow ) {
24239 return;
24240 }
24241
24242 $vars = array( 'id' => $this->_module_id );
24243 fs_require_once_template( 'connect.php', $vars );
24244 }
24245
24246 /* Action Links
24247 ------------------------------------------------------------------------------------------------------------------*/
24248 private $_action_links_hooked = false;
24249 private $_action_links = array();
24250
24251 /**
24252 * Hook to plugin action links filter.
24253 *
24254 * @author Vova Feldman (@svovaf)
24255 * @since 1.0.0
24256 */
24257 private function hook_plugin_action_links() {
24258 $this->_logger->entrance();
24259
24260 $this->_action_links_hooked = true;
24261
24262 $this->_logger->log( 'Adding action links hooks.' );
24263
24264 // Add action link to settings page.
24265 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24266 &$this,
24267 '_modify_plugin_action_links_hook'
24268 ), WP_FS__DEFAULT_PRIORITY, 2 );
24269 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24270 &$this,
24271 '_modify_plugin_action_links_hook'
24272 ), WP_FS__DEFAULT_PRIORITY, 2 );
24273 }
24274
24275 /**
24276 * Add plugin action link.
24277 *
24278 * @author Vova Feldman (@svovaf)
24279 * @since 1.0.0
24280 *
24281 * @param $label
24282 * @param $url
24283 * @param bool $external
24284 * @param int $priority
24285 * @param bool $key
24286 */
24287 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24288 $this->_logger->entrance();
24289
24290 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24291 $this->_action_links[ $priority ] = array();
24292 }
24293
24294 if ( false === $key ) {
24295 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24296 }
24297
24298 $this->_action_links[ $priority ][] = array(
24299 'label' => $label,
24300 'href' => $url,
24301 'key' => $key,
24302 'external' => $external
24303 );
24304 }
24305
24306 /**
24307 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24308 *
24309 * @author Vova Feldman (@svovaf)
24310 * @since 1.0.0
24311 */
24312 function _add_upgrade_action_link() {
24313 $this->_logger->entrance();
24314
24315 $is_activation_mode = $this->is_activation_mode();
24316
24317 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24318
24319 /**
24320 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24321 * and "Add-Ons" menus should be added.
24322 *
24323 * @author Leo Fajardo (@leorw)
24324 * @since 2.3.0
24325 */
24326 $add_upgrade_link = (
24327 $add_action_links ||
24328 ( $is_activation_mode && $this->is_only_premium() )
24329 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24330
24331 $add_addons_link = ( $add_action_links && $this->has_addons() );
24332
24333 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24334 return;
24335 }
24336
24337 if (
24338 $add_upgrade_link &&
24339 $this->is_pricing_page_visible() &&
24340 $this->is_submenu_item_visible( 'pricing' )
24341 ) {
24342 $this->add_plugin_action_link(
24343 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24344 $this->get_upgrade_url(),
24345 false,
24346 7,
24347 'upgrade'
24348 );
24349 }
24350
24351 if (
24352 $add_addons_link &&
24353 $this->has_addons() &&
24354 $this->is_submenu_item_visible( 'addons' )
24355 ) {
24356 $this->add_plugin_action_link(
24357 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24358 $this->_get_admin_page_url( 'addons' ),
24359 false,
24360 9,
24361 'addons'
24362 );
24363 }
24364 }
24365
24366 /**
24367 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24368 *
24369 * @author Leo Fajardo (@leorw)
24370 * @since 1.1.9
24371 */
24372 function _add_license_action_link() {
24373 $this->_logger->entrance();
24374
24375 if ( ! self::is_ajax() ) {
24376 // Inject license activation dialog UI and client side code.
24377 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24378 }
24379
24380 $link_text = $this->is_free_plan() ?
24381 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24382 $this->get_text_inline( 'Change License', 'change-license' );
24383
24384 $this->add_plugin_action_link(
24385 $link_text,
24386 '#',
24387 false,
24388 11,
24389 ( 'activate-license ' . $this->get_unique_affix() )
24390 );
24391 }
24392
24393 /**
24394 * @author Leo Fajardo (@leorw)
24395 * @since 2.0.2
24396 */
24397 function _add_premium_version_upgrade_selection_action() {
24398 $this->_logger->entrance();
24399
24400 if ( ! self::is_ajax() ) {
24401 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24402 }
24403 }
24404
24405 /**
24406 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24407 *
24408 * @author Leo Fajardo (@leorw)
24409 * @since 1.2.1.5
24410 */
24411 function _add_tracking_links() {
24412 if ( ! current_user_can( 'manage_options' ) ) {
24413 return;
24414 }
24415
24416 $this->_logger->entrance();
24417
24418 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24419 // 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.
24420 return;
24421 }
24422
24423 if (
24424 $this->is_addon() &&
24425 ! $this->is_only_premium()
24426 ) {
24427 $parent = $this->get_parent_instance();
24428
24429 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24430 return;
24431 }
24432 }
24433
24434 if ( fs_is_network_admin() ) {
24435 if ( ! $this->_is_network_active ) {
24436 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24437 return;
24438 } else if ( $this->is_network_delegated_connection() ) {
24439 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24440 return;
24441 }
24442 } else {
24443 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24444 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24445 return;
24446 }
24447 }
24448
24449 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24450 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24451 $this->connect_again();
24452
24453 return;
24454 }
24455 }
24456
24457 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24458 ( $this->is_theme() && ! self::is_themes_page() )
24459 ) {
24460 // Only show tracking links on the plugins and themes pages.
24461 return;
24462 }
24463
24464 if (
24465 $this->is_activation_mode() &&
24466 $this->is_premium() &&
24467 ! $this->is_registered()
24468 ) {
24469 // If not yet registered and running the premium code base, a license activation link will already be shown.
24470 return;
24471 }
24472
24473 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24474 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24475 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24476 return;
24477 }
24478 }
24479
24480 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24481 return;
24482 }
24483
24484 $link_text_id = '';
24485 $url = '#';
24486
24487 if ( $this->is_registered( true ) ) {
24488 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24489 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24490 } else {
24491 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24492 }
24493 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24494 /**
24495 * Show opt-in link only if skipped or in activation mode.
24496 */
24497 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24498
24499 $params = ! $this->is_anonymous() ?
24500 array() :
24501 array(
24502 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24503 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24504 );
24505
24506 $url = $this->get_activation_url( $params );
24507 }
24508
24509 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24510
24511 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24512 $this->add_plugin_action_link(
24513 $link_text_id,
24514 $url,
24515 false,
24516 13,
24517 "opt-in-or-opt-out {$this->_slug}"
24518 );
24519 }
24520 }
24521
24522 /**
24523 * Get the URL of the page that should be loaded right after the plugin activation.
24524 *
24525 * @author Vova Feldman (@svovaf)
24526 * @since 1.1.7.4
24527 *
24528 * @return string
24529 */
24530 function get_after_plugin_activation_redirect_url() {
24531 $url = false;
24532
24533 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24534 $first_time_path = $this->_menu->get_first_time_path(
24535 fs_is_network_admin() && $this->_is_network_active
24536 );
24537
24538 if ( $this->is_activation_mode() ) {
24539 $url = $this->get_activation_url();
24540 } else if ( ! empty( $first_time_path ) ) {
24541 $url = $first_time_path;
24542 } else {
24543 $page = '';
24544 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24545 if ( $this->is_network_registered() ) {
24546 $page = 'account';
24547 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24548 $this->maybe_set_slug_and_network_menu_exists_flag();
24549 }
24550 }
24551
24552 $url = $this->_get_admin_page_url( $page );
24553 }
24554 } else {
24555 $plugin_fs = false;
24556
24557 if ( $this->is_parent_plugin_installed() ) {
24558 $plugin_fs = self::get_parent_instance();
24559 }
24560
24561 if ( is_object( $plugin_fs ) ) {
24562 if ( ! $plugin_fs->is_registered() ) {
24563 // Forward to parent plugin connect when parent not registered.
24564 $url = $plugin_fs->get_activation_url();
24565 } else {
24566 // Forward to account page.
24567 $url = $plugin_fs->_get_admin_page_url( 'account' );
24568 }
24569 }
24570 }
24571
24572 return $url;
24573 }
24574
24575 /**
24576 * Forward page to activation page.
24577 *
24578 * @author Vova Feldman (@svovaf)
24579 * @since 1.0.3
24580 */
24581 function _redirect_on_activation_hook() {
24582 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24583 $url = $this->get_after_plugin_activation_redirect_url();
24584
24585 if ( is_string( $url ) ) {
24586 fs_redirect( $url );
24587 }
24588 }
24589 }
24590
24591 /**
24592 * Modify plugin's page action links collection.
24593 *
24594 * @author Vova Feldman (@svovaf)
24595 * @since 1.0.0
24596 *
24597 * @param array $links
24598 * @param $file
24599 *
24600 * @return array
24601 */
24602 function _modify_plugin_action_links_hook( $links, $file ) {
24603 $this->_logger->entrance();
24604
24605 $passed_deactivate = false;
24606 $deactivate_link = '';
24607 $before_deactivate = array();
24608 $after_deactivate = array();
24609 foreach ( $links as $key => $link ) {
24610 if ( 'deactivate' === $key ) {
24611 $deactivate_link = $link;
24612 $passed_deactivate = true;
24613 continue;
24614 }
24615
24616 if ( ! $passed_deactivate ) {
24617 $before_deactivate[ $key ] = $link;
24618 } else {
24619 $after_deactivate[ $key ] = $link;
24620 }
24621 }
24622
24623 ksort( $this->_action_links );
24624
24625 foreach ( $this->_action_links as $new_links ) {
24626 foreach ( $new_links as $link ) {
24627 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24628 }
24629 }
24630
24631 if ( ! empty( $deactivate_link ) ) {
24632 /**
24633 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24634 *
24635 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24636 */
24637 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24638
24639 // Append deactivation link.
24640 $before_deactivate['deactivate'] = $deactivate_link;
24641 }
24642
24643 return array_merge( $before_deactivate, $after_deactivate );
24644 }
24645
24646 /**
24647 * Adds admin message.
24648 *
24649 * @author Vova Feldman (@svovaf)
24650 * @since 1.0.4
24651 *
24652 * @param string $message
24653 * @param string $title
24654 * @param string $type
24655 */
24656 function add_admin_message( $message, $title = '', $type = 'success' ) {
24657 $this->_admin_notices->add( $message, $title, $type );
24658 }
24659
24660 /**
24661 * Adds sticky admin message.
24662 *
24663 * @author Vova Feldman (@svovaf)
24664 * @since 1.1.0
24665 *
24666 * @param string $message
24667 * @param string $id
24668 * @param string $title
24669 * @param string $type
24670 */
24671 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24672 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24673 }
24674
24675 /**
24676 * Check if the paid version of the module is installed.
24677 *
24678 * @author Vova Feldman (@svovaf)
24679 * @since 2.2.0
24680 *
24681 * @return bool
24682 */
24683 private function is_premium_version_installed() {
24684 $premium_plugin_basename = $this->premium_plugin_basename();
24685
24686 if ( $this->is_theme() ) {
24687 return $this->can_activate_theme( $this->get_premium_slug() );
24688 }
24689
24690 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24691 }
24692
24693 /**
24694 * Helper function that returns the final steps for the upgrade completion.
24695 *
24696 * If the module is already running the premium code, returns an empty string.
24697 *
24698 * @author Vova Feldman (@svovaf)
24699 * @since 1.2.1
24700 *
24701 * @param string $plan_title
24702 *
24703 * @return string
24704 */
24705 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24706 $this->_logger->entrance();
24707
24708 $activate_license_string = $this->get_license_network_activation_notice();
24709
24710 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24711 return '' . $activate_license_string;
24712 }
24713
24714 if ( empty( $plan_title ) ) {
24715 $plan_title = $this->get_plan_title();
24716 }
24717
24718 if ( $this->is_premium_version_installed() ) {
24719 /**
24720 * If the premium version is already installed, instead of showing the installation instructions,
24721 * tell the current user to activate it.
24722 *
24723 * @author Leo Fajardo (@leorw)
24724 * @since 2.2.1
24725 */
24726 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24727 $this->get_premium_slug() :
24728 $this->premium_plugin_basename();
24729
24730 return sprintf(
24731 /* translators: %1$s: Product title; %2$s: Plan title */
24732 $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' ),
24733 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24734 $plan_title,
24735 sprintf(
24736 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24737 ( $this->is_theme() ?
24738 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24739 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24740 esc_html( sprintf(
24741 /* translators: %s: Plan title */
24742 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24743 $plan_title
24744 ) )
24745 )
24746 );
24747 } else {
24748 // @since 1.2.1.5 The free version is auto deactivated.
24749 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24750 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24751 '';
24752
24753 return sprintf(
24754 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24755 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24756 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24757 $this->get_latest_download_link( sprintf(
24758 /* translators: %s: Plan title */
24759 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24760 $plan_title
24761 ) ),
24762 $deactivation_step,
24763 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24764 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24765 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24766 );
24767 }
24768 }
24769
24770 /**
24771 * @author Leo Fajardo (@leorw)
24772 * @since 2.5.3
24773 *
24774 * @param string $message_before_the_instructions
24775 * @param string $message_id
24776 * @param string $plan_title
24777 */
24778 private function add_complete_upgrade_instructions_notice(
24779 $message_before_the_instructions,
24780 $message_id,
24781 $plan_title = ''
24782 ) {
24783 $this->_admin_notices->add_sticky(
24784 $message_before_the_instructions .
24785 $this->get_complete_upgrade_instructions( $plan_title ),
24786 $message_id,
24787 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24788 );
24789 }
24790
24791 /**
24792 * @author Leo Fajardo (@leorw)
24793 * @since 2.5.3
24794 *
24795 * @param bool $is_upgrade
24796 */
24797 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24798 $this->add_complete_upgrade_instructions_notice(
24799 $is_upgrade ?
24800 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24801 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24802 'plan_upgraded'
24803 );
24804 }
24805
24806 /**
24807 * @author Leo Fajardo (@leorw)
24808 * @since 2.1.0
24809 *
24810 * @param string $url
24811 * @param array $request
24812 */
24813 private static function enrich_request_for_debug( &$url, &$request ) {
24814 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
24815 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
24816 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
24817
24818 $request['cookies'] = array(
24819 new WP_Http_Cookie( array(
24820 'name' => 'XDEBUG_SESSION',
24821 'value' => 'PHPSTORM',
24822 ) )
24823 );
24824 }
24825 }
24826
24827 /**
24828 * @author Leo Fajardo (@leorw)
24829 * @since 2.1.0
24830 *
24831 * @param string $url
24832 * @param array $request
24833 * @param int $success_cache_expiration
24834 * @param int $failure_cache_expiration
24835 * @param bool $maybe_enrich_request_for_debug
24836 *
24837 * @return WP_Error|array
24838 */
24839 static function safe_remote_post(
24840 &$url,
24841 $request,
24842 $success_cache_expiration = 0,
24843 $failure_cache_expiration = 0,
24844 $maybe_enrich_request_for_debug = true
24845 ) {
24846 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24847
24848 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24849
24850 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24851 get_transient( $cache_key ) :
24852 false;
24853
24854 if ( false === $response ) {
24855 if ( $maybe_enrich_request_for_debug ) {
24856 self::enrich_request_for_debug( $url, $request );
24857 }
24858
24859 if ( ! isset( $request['method'] ) ) {
24860 $request['method'] = 'POST';
24861 }
24862
24863 $response = FS_Api::remote_request( $url, $request );
24864
24865 if (
24866 'https://' === substr( $url, 0, 8 ) &&
24867 FS_Api::is_ssl_error_response( $response )
24868 ) {
24869 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24870 $url = 'http://' . substr( $url, 8 );
24871
24872 $request['timeout'] = 15;
24873 $response = FS_Api::remote_request( $url, $request );
24874 }
24875
24876 if ( false !== $cache_key ) {
24877 set_transient(
24878 $cache_key,
24879 $response,
24880 ( ( $response instanceof WP_Error ) ?
24881 $failure_cache_expiration :
24882 $success_cache_expiration )
24883 );
24884 }
24885 }
24886
24887 return $response;
24888 }
24889
24890 /**
24891 * This method is used to enrich the after upgrade notice instructions when the upgraded
24892 * license cannot be activated network wide (license quota isn't large enough).
24893 *
24894 * @author Vova Feldman (@svovaf)
24895 * @since 2.0.0
24896 *
24897 * @return string
24898 */
24899 private function get_license_network_activation_notice() {
24900 if ( ! $this->_is_network_active ) {
24901 // Module isn't network level activated.
24902 return '';
24903 }
24904
24905 if ( ! fs_is_network_admin() ) {
24906 // Not network level admin.
24907 return '';
24908 }
24909
24910 if ( get_blog_count() == 1 ) {
24911 // There's only a single site in the network so if there's a context license it was already activated.
24912 return '';
24913 }
24914
24915 if ( ! is_object( $this->_license ) ) {
24916 // No context license.
24917 return '';
24918 }
24919
24920 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24921 // 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).
24922 return '';
24923 }
24924
24925 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24926 // 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).
24927 return '';
24928 }
24929
24930 return sprintf(
24931 $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' ),
24932 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24933 '</a>'
24934 );
24935 }
24936
24937 /**
24938 * @author Vova Feldman (@svovaf)
24939 * @since 1.2.1.7
24940 *
24941 * @param string $key
24942 *
24943 * @return string
24944 */
24945 function get_text( $key ) {
24946 return fs_text( $key, $this->_slug );
24947 }
24948
24949 /**
24950 * @author Vova Feldman (@svovaf)
24951 * @since 1.2.3
24952 *
24953 * @param string $text Translatable string.
24954 * @param string $key String key for overrides.
24955 *
24956 * @return string
24957 */
24958 function get_text_inline( $text, $key = '' ) {
24959 return _fs_text_inline( $text, $key, $this->_slug );
24960 }
24961
24962 /**
24963 * @author Vova Feldman (@svovaf)
24964 * @since 1.2.3
24965 *
24966 * @param string $text Translatable string.
24967 * @param string $context Context information for the translators.
24968 * @param string $key String key for overrides.
24969 *
24970 * @return string
24971 */
24972 function get_text_x_inline( $text, $context, $key ) {
24973 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24974 }
24975
24976 /**
24977 * @author Vova Feldman (@svovaf)
24978 * @since 1.2.3
24979 *
24980 * @param string $text Translatable string.
24981 * @param string $key String key for overrides.
24982 *
24983 * @return string
24984 */
24985 function esc_html_inline( $text, $key ) {
24986 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
24987 }
24988
24989 #----------------------------------------------------------------------------------
24990 #region Versioning
24991 #----------------------------------------------------------------------------------
24992
24993 /**
24994 * Check if Freemius in SDK upgrade mode.
24995 *
24996 * @author Vova Feldman (@svovaf)
24997 * @since 1.0.9
24998 *
24999 * @return bool
25000 */
25001 function is_sdk_upgrade_mode() {
25002 return isset( $this->_storage->sdk_upgrade_mode ) ?
25003 $this->_storage->sdk_upgrade_mode :
25004 false;
25005 }
25006
25007 /**
25008 * Turn SDK upgrade mode off.
25009 *
25010 * @author Vova Feldman (@svovaf)
25011 * @since 1.0.9
25012 */
25013 function set_sdk_upgrade_complete() {
25014 $this->_storage->sdk_upgrade_mode = false;
25015 }
25016
25017 /**
25018 * Check if plugin upgrade mode.
25019 *
25020 * @author Vova Feldman (@svovaf)
25021 * @since 1.0.9
25022 *
25023 * @return bool
25024 */
25025 function is_plugin_upgrade_mode() {
25026 return isset( $this->_storage->plugin_upgrade_mode ) ?
25027 $this->_storage->plugin_upgrade_mode :
25028 false;
25029 }
25030
25031 /**
25032 * Turn plugin upgrade mode off.
25033 *
25034 * @author Vova Feldman (@svovaf)
25035 * @since 1.0.9
25036 */
25037 function set_plugin_upgrade_complete() {
25038 $this->_storage->plugin_upgrade_mode = false;
25039
25040 $license_migration = ! empty( $this->_storage->license_migration ) ?
25041 $this->_storage->license_migration :
25042 array();
25043
25044 $license_migration['is_migrating'] = false;
25045
25046 $this->_storage->license_migration = $license_migration;
25047 }
25048
25049 #endregion
25050
25051 #----------------------------------------------------------------------------------
25052 #region Permissions
25053 #----------------------------------------------------------------------------------
25054
25055 /**
25056 * Check if specific permission requested.
25057 *
25058 * @author Vova Feldman (@svovaf)
25059 * @since 1.1.6
25060 *
25061 * @param string $permission
25062 *
25063 * @return bool
25064 */
25065 function is_permission_requested( $permission ) {
25066 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25067 }
25068
25069 #endregion
25070
25071 #----------------------------------------------------------------------------------
25072 #region Auto Activation
25073 #----------------------------------------------------------------------------------
25074
25075 /**
25076 * Hints the SDK if running an auto-installation.
25077 *
25078 * @var bool
25079 */
25080 private $_isAutoInstall = false;
25081
25082 /**
25083 * After upgrade callback to install and auto activate a plugin.
25084 * This code will only be executed on explicit request from the user,
25085 * following the practice Jetpack are using with their theme installations.
25086 *
25087 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25088 *
25089 * @author Vova Feldman (@svovaf)
25090 * @since 1.2.1.7
25091 */
25092 function _install_premium_version_ajax_action() {
25093 $this->_logger->entrance();
25094
25095 $this->check_ajax_referer( 'install_premium_version' );
25096
25097 if ( ! $this->is_registered() ) {
25098 // Not registered.
25099 self::shoot_ajax_failure( array(
25100 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25101 'code' => 'premium_installed',
25102 ) );
25103 }
25104
25105 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25106
25107 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25108 // Invalid ID.
25109 self::shoot_ajax_failure( array(
25110 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25111 'code' => 'invalid_module_id',
25112 ) );
25113 }
25114
25115 if ( $plugin_id == $this->get_id() ) {
25116 if ( $this->is_premium() ) {
25117 // Already using the premium code version.
25118 self::shoot_ajax_failure( array(
25119 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25120 'code' => 'premium_installed',
25121 ) );
25122 }
25123 if ( ! $this->can_use_premium_code() ) {
25124 // Don't have access to the premium code.
25125 self::shoot_ajax_failure( array(
25126 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25127 'code' => 'invalid_license',
25128 ) );
25129 }
25130 if ( ! $this->has_release_on_freemius() ) {
25131 // Plugin is a serviceware, no premium code version.
25132 self::shoot_ajax_failure( array(
25133 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25134 'code' => 'premium_version_missing',
25135 ) );
25136 }
25137 } else {
25138 $addon = $this->get_addon( $plugin_id );
25139
25140 if ( ! is_object( $addon ) ) {
25141 // Invalid add-on ID.
25142 self::shoot_ajax_failure( array(
25143 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25144 'code' => 'invalid_module_id',
25145 ) );
25146 }
25147
25148 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25149 // Premium add-on version is already activated.
25150 self::shoot_ajax_failure( array(
25151 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25152 'code' => 'premium_installed',
25153 ) );
25154 }
25155 }
25156
25157 $this->_isAutoInstall = true;
25158
25159 // Try to install and activate.
25160 $updater = FS_Plugin_Updater::instance( $this );
25161 $result = $updater->install_and_activate_plugin( $plugin_id );
25162
25163 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25164 self::shoot_ajax_failure( array(
25165 'message' => $result['message'],
25166 'code' => $result['code'],
25167 ) );
25168 }
25169
25170 self::shoot_ajax_success( $result );
25171 }
25172
25173 /**
25174 * Displays module activation dialog box after a successful upgrade
25175 * where the user explicitly requested to auto download and install
25176 * the premium version.
25177 *
25178 * @author Vova Feldman (@svovaf)
25179 * @since 1.2.1.7
25180 */
25181 function _add_auto_installation_dialog_box() {
25182 $this->_logger->entrance();
25183
25184 if ( ! $this->is_registered() ) {
25185 // Not registered.
25186 return;
25187 }
25188
25189 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25190
25191 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25192 // Invalid module ID.
25193 return;
25194 }
25195
25196 if ( $plugin_id == $this->get_id() ) {
25197 if ( $this->is_premium() ) {
25198 // Already using the premium code version.
25199 return;
25200 }
25201 if ( ! $this->can_use_premium_code() ) {
25202 // Don't have access to the premium code.
25203 return;
25204 }
25205 if ( ! $this->has_release_on_freemius() ) {
25206 // Plugin is a serviceware, no premium code version.
25207 return;
25208 }
25209 } else {
25210 $addon = $this->get_addon( $plugin_id );
25211
25212 if ( ! is_object( $addon ) ) {
25213 // Invalid add-on ID.
25214 return;
25215 }
25216
25217 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25218 // Premium add-on version is already activated.
25219 return;
25220 }
25221 }
25222
25223 $vars = array(
25224 'id' => $this->_module_id,
25225 'target_module_id' => $plugin_id,
25226 'slug' => $this->_slug,
25227 );
25228
25229 fs_require_template( 'auto-installation.php', $vars );
25230 }
25231
25232 #endregion
25233
25234 #--------------------------------------------------------------------------------
25235 #region Tabs Integration
25236 #--------------------------------------------------------------------------------
25237
25238 #region Module's Original Tabs
25239
25240 /**
25241 * Inject a JavaScript logic to capture the theme tabs HTML.
25242 *
25243 * @author Vova Feldman (@svovaf)
25244 * @since 1.2.2.7
25245 */
25246 function _tabs_capture() {
25247 $this->_logger->entrance();
25248
25249 if (
25250 ! $this->is_product_settings_page() ||
25251 ! $this->should_page_include_tabs() ||
25252 ! $this->is_matching_url( $this->main_menu_url() )
25253 ) {
25254 return;
25255 }
25256
25257 $params = array(
25258 'id' => $this->_module_id,
25259 );
25260
25261 fs_require_once_template( 'tabs-capture-js.php', $params );
25262 }
25263
25264 /**
25265 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25266 * after version and type (free/premium) changes, in addition to the week period.
25267 *
25268 * @author Vova Feldman (@svovaf)
25269 * @since 1.2.2.7
25270 */
25271 function _store_tabs_ajax_action() {
25272 $this->_logger->entrance();
25273
25274 $this->check_ajax_referer( 'store_tabs' );
25275
25276 // Init filesystem if not yet initiated.
25277 WP_Filesystem();
25278
25279 // Get POST body HTML data.
25280 global $wp_filesystem;
25281 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25282
25283 if ( is_string( $tabs_html ) ) {
25284 $tabs_html = trim( $tabs_html );
25285 }
25286
25287 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25288 self::shoot_ajax_failure();
25289 }
25290
25291 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25292
25293 self::shoot_ajax_success();
25294 }
25295
25296 /**
25297 * Cache theme's settings page custom styles. The cache will also be set as expired
25298 * after version and type (free/premium) changes, in addition to the week period.
25299 *
25300 * @author Vova Feldman (@svovaf)
25301 * @since 1.2.2.7
25302 */
25303 function _store_tabs_styles() {
25304 $this->_logger->entrance();
25305
25306 if (
25307 ! $this->is_product_settings_page() ||
25308 ! $this->should_page_include_tabs() ||
25309 ! $this->is_matching_url( $this->main_menu_url() )
25310 ) {
25311 return;
25312 }
25313
25314 $wp_styles = wp_styles();
25315
25316 $theme_styles_url = get_template_directory_uri();
25317
25318 $stylesheets = array();
25319 foreach ( $wp_styles->queue as $handler ) {
25320 if ( fs_starts_with( $handler, 'fs_' ) ) {
25321 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25322 continue;
25323 }
25324
25325 /**
25326 * @var _WP_Dependency $stylesheet
25327 */
25328 $stylesheet = $wp_styles->registered[ $handler ];
25329
25330 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25331 $stylesheets[] = $stylesheet->src;
25332 }
25333 }
25334
25335 if ( ! empty( $stylesheets ) ) {
25336 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25337 }
25338 }
25339
25340 /**
25341 * Check if module's original settings page has any tabs.
25342 *
25343 * @author Vova Feldman (@svovaf)
25344 * @since 1.2.2.7
25345 *
25346 * @return bool
25347 */
25348 private function has_tabs() {
25349 return $this->_cache->has( 'tabs' );
25350 }
25351
25352 /**
25353 * Get module's settings page HTML content, starting
25354 * from the beginning of the <div class="wrap"> element,
25355 * until the tabs HTML (including).
25356 *
25357 * @author Vova Feldman (@svovaf)
25358 * @since 1.2.2.7
25359 *
25360 * @return string
25361 */
25362 private function get_tabs_html() {
25363 $this->_logger->entrance();
25364
25365 return $this->_cache->get( 'tabs' );
25366 }
25367
25368 /**
25369 * Check if page should include tabs.
25370 *
25371 * @author Vova Feldman (@svovaf)
25372 * @since 1.2.2.7
25373 *
25374 * @return bool
25375 */
25376 private function should_page_include_tabs() {
25377 if ( ! $this->has_settings_menu() ) {
25378 // Don't add tabs if no settings at all.
25379 return false;
25380 }
25381
25382 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25383 // Only add tabs to themes for now.
25384 return false;
25385 }
25386
25387 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25388 // Only add tabs to monetizing themes.
25389 return false;
25390 }
25391
25392 if ( ! $this->is_product_settings_page() ) {
25393 // Only add tabs if browsing one of the product's setting pages.
25394 return false;
25395 }
25396
25397 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25398 // Don't include tabs in the activation page.
25399 return false;
25400 }
25401
25402 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25403 // Don't add tabs on checkout page, we want to reduce distractions
25404 // as much as possible.
25405 return false;
25406 }
25407
25408 return true;
25409 }
25410
25411 /**
25412 * Add the tabs HTML before the setting's page content and
25413 * enqueue any required stylesheets.
25414 *
25415 * @author Vova Feldman (@svovaf)
25416 * @since 1.2.2.7
25417 *
25418 * @return bool If tabs were included.
25419 */
25420 function _add_tabs_before_content() {
25421 $this->_logger->entrance();
25422
25423 if ( ! $this->should_page_include_tabs() ) {
25424 return false;
25425 }
25426
25427 /**
25428 * Enqueue the original stylesheets that are included in the
25429 * theme settings page. That way, if the theme settings has
25430 * some custom _styled_ content above the tabs UI, this
25431 * will make sure that the styling is preserved.
25432 */
25433 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25434 if ( is_array( $stylesheets ) ) {
25435 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25436 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25437 }
25438 }
25439
25440 // Cut closing </div> tag.
25441 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
25442
25443 return true;
25444 }
25445
25446 /**
25447 * Add the tabs closing HTML after the setting's page content.
25448 *
25449 * @author Vova Feldman (@svovaf)
25450 * @since 1.2.2.7
25451 *
25452 * @return bool If tabs closing HTML was included.
25453 */
25454 function _add_tabs_after_content() {
25455 $this->_logger->entrance();
25456
25457 if ( ! $this->should_page_include_tabs() ) {
25458 return false;
25459 }
25460
25461 echo '</div>';
25462
25463 return true;
25464 }
25465
25466 #endregion
25467
25468 /**
25469 * Add in-page JavaScript to inject the Freemius tabs into
25470 * the module's setting tabs section.
25471 *
25472 * @author Vova Feldman (@svovaf)
25473 * @since 1.2.2.7
25474 */
25475 function _add_freemius_tabs() {
25476 $this->_logger->entrance();
25477
25478 if ( ! $this->should_page_include_tabs() ) {
25479 return;
25480 }
25481
25482 $params = array( 'id' => $this->_module_id );
25483 fs_require_once_template( 'tabs.php', $params );
25484 }
25485
25486 #endregion
25487
25488 #--------------------------------------------------------------------------------
25489 #region Customizer Integration for Themes
25490 #--------------------------------------------------------------------------------
25491
25492 /**
25493 * @author Vova Feldman (@svovaf)
25494 * @since 1.2.2.7
25495 *
25496 * @param WP_Customize_Manager $customizer
25497 */
25498 function _customizer_register( $customizer ) {
25499 $this->_logger->entrance();
25500
25501 if ( $this->is_pricing_page_visible() ) {
25502 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25503
25504 $customizer->add_section( 'freemius_upsell', array(
25505 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25506 'priority' => 1,
25507 ) );
25508 $customizer->add_setting( 'freemius_upsell', array(
25509 'sanitize_callback' => 'esc_html',
25510 ) );
25511
25512 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25513 'fs' => $this,
25514 'section' => 'freemius_upsell',
25515 'priority' => 100,
25516 ) ) );
25517 }
25518
25519 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25520 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25521
25522 // Main Documentation Link In Customizer Root.
25523 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25524 'fs' => $this,
25525 'priority' => 1000,
25526 ) ) );
25527 }
25528 }
25529
25530 #endregion
25531
25532 /**
25533 * If the theme has a paid version, add some custom
25534 * styling to the theme's premium version (if exists)
25535 * to highlight that it's the premium version of the
25536 * same theme, making it easier for identification
25537 * after the user upgrades and upload it to the site.
25538 *
25539 * @author Vova Feldman (@svovaf)
25540 * @since 1.2.2.7
25541 */
25542 function _style_premium_theme() {
25543 $this->_logger->entrance();
25544
25545 if ( ! self::is_themes_page() ) {
25546 // Only include in the themes page.
25547 return;
25548 }
25549
25550 if ( ! $this->has_paid_plan() ) {
25551 // Only include if has any paid plans.
25552 return;
25553 }
25554
25555 $params = null;
25556 fs_require_once_template( '/js/jquery.content-change.php', $params );
25557
25558 $params = array(
25559 'slug' => $this->_slug,
25560 'id' => $this->_module_id,
25561 );
25562
25563 fs_require_template( '/js/style-premium-theme.php', $params );
25564 }
25565
25566 /**
25567 * This method will return the absolute URL of the module's local icon.
25568 *
25569 * When you are running your plugin or theme on a **localhost** environment, if the icon
25570 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25571 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25572 * If an icon is found, this method will automatically attempt to download the icon and store it
25573 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25574 *
25575 * It's important to mention that this method is NOT phoning home since the developer will deploy
25576 * the product with the local icon in the assets folder. The download process just simplifies
25577 * the process for the developer.
25578 *
25579 * @author Vova Feldman (@svovaf)
25580 * @since 2.0.0
25581 *
25582 * @return string
25583 */
25584 function get_local_icon_url() {
25585 global $fs_active_plugins;
25586
25587 /**
25588 * @since 1.1.7.5
25589 */
25590 $local_path = $this->apply_filters( 'plugin_icon', false );
25591
25592 if ( is_string( $local_path ) ) {
25593 $icons = array( $local_path );
25594 } else {
25595 $img_dir = WP_FS__DIR_IMG;
25596
25597 // Locate the main assets folder.
25598 if ( 1 < count( $fs_active_plugins->plugins ) ) {
25599 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25600
25601 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25602 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25603 $img_dir = $plugin_or_theme_img_dir
25604 . '/'
25605 /**
25606 * The basename will be `themes` or the basename of a custom themes directory.
25607 *
25608 * @author Leo Fajardo (@leorw)
25609 * @since 2.2.3
25610 */
25611 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25612 . '/assets/img';
25613
25614 break;
25615 }
25616 }
25617 }
25618
25619 // Try to locate the icon in the assets folder.
25620 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25621
25622 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25623 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25624 $icons = array(
25625 fs_normalize_path( $img_dir . '/theme-icon.png' )
25626 );
25627 } else {
25628 $icon_found = false;
25629 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25630
25631 if ( ! function_exists( 'get_filesystem_method' ) ) {
25632 require_once ABSPATH . 'wp-admin/includes/file.php';
25633 }
25634
25635 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25636
25637 /**
25638 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25639 *
25640 * This code will only be executed once during the testing
25641 * of the plugin in a local environment. The plugin icon file WILL
25642 * already exist in the assets folder when the plugin is deployed to
25643 * the repository.
25644 */
25645 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25646 // Fetch icon from Freemius.
25647 $icon = $this->fetch_remote_icon_url();
25648
25649 // Fetch icon from WordPress.org.
25650 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25651 if ( ! function_exists( 'plugins_api' ) ) {
25652 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25653 }
25654
25655 $plugin_information = plugins_api( 'plugin_information', array(
25656 'slug' => $this->_slug,
25657 'fields' => array(
25658 'sections' => false,
25659 'tags' => false,
25660 'icons' => true
25661 )
25662 ) );
25663
25664 if (
25665 ! is_wp_error( $plugin_information )
25666 && isset( $plugin_information->icons )
25667 && ! empty( $plugin_information->icons )
25668 ) {
25669 /**
25670 * Get the smallest icon.
25671 *
25672 * @author Leo Fajardo (@leorw)
25673 * @since 1.2.2
25674 */
25675 $icon = end( $plugin_information->icons );
25676 }
25677 }
25678
25679 if ( ! empty( $icon ) ) {
25680 if ( 0 !== strpos( $icon, 'http' ) ) {
25681 $icon = 'http:' . $icon;
25682 }
25683
25684 /**
25685 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25686 *
25687 * @author Leo Fajardo (@leorw)
25688 * @since 1.2.2
25689 */
25690 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25691
25692 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25693
25694 // Try to download the icon.
25695 $icon_found = fs_download_image( $icon, $local_path );
25696 }
25697 }
25698
25699 if ( ! $icon_found ) {
25700 // No icons found, fallback to default icon.
25701 if ( $have_write_permissions ) {
25702 // If have write permissions, copy default icon.
25703 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25704 } else {
25705 // If doesn't have write permissions, use default icon path.
25706 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25707 }
25708 }
25709
25710 $icons = array( $local_path );
25711 }
25712 }
25713 }
25714
25715 $icon_dir = dirname( $icons[0] );
25716
25717 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25718 }
25719
25720 /**
25721 * Fetch module's extended info.
25722 *
25723 * @author Vova Feldman (@svovaf)
25724 * @since 2.0.0
25725 *
25726 * @return object|mixed
25727 */
25728 private function fetch_module_info() {
25729 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25730 }
25731
25732 /**
25733 * Fetch module's remote icon URL.
25734 *
25735 * @author Vova Feldman (@svovaf)
25736 * @since 2.0.0
25737 *
25738 * @return string
25739 */
25740 function fetch_remote_icon_url() {
25741 $info = $this->fetch_module_info();
25742
25743 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25744 $info->icon :
25745 '';
25746 }
25747
25748 #--------------------------------------------------------------------------------
25749 #region GDPR
25750 #--------------------------------------------------------------------------------
25751
25752 /**
25753 * @author Leo Fajardo (@leorw)
25754 * @since 2.1.0
25755 *
25756 * @param array $user_plugins
25757 *
25758 * @return string
25759 */
25760 private function get_gdpr_admin_notice_string( $user_plugins ) {
25761 $this->_logger->entrance();
25762
25763 $addons = self::get_all_addons();
25764
25765 foreach ( $user_plugins as $user_plugin ) {
25766 $has_addons = isset( $addons[ $user_plugin->id ] );
25767
25768 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25769 if ( $this->_module_id == $user_plugin->id ) {
25770 $addons = $this->get_addons();
25771 $has_addons = ( ! empty( $addons ) );
25772 } else {
25773 $plugin_api = FS_Api::instance(
25774 $user_plugin->id,
25775 'plugin',
25776 $user_plugin->id,
25777 $user_plugin->public_key,
25778 ! $user_plugin->is_live,
25779 false,
25780 $this->get_sdk_version()
25781 );
25782
25783 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25784
25785 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25786 is_array( $addons_result->plugins ) &&
25787 ! empty( $addons_result->plugins )
25788 ) {
25789 $has_addons = true;
25790 }
25791 }
25792 }
25793
25794 $user_plugin->has_addons = $has_addons;
25795 }
25796
25797 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25798
25799 $multiple_products_text = '';
25800
25801 if ( $is_single_parent_product ) {
25802 $single_parent_product = reset( $user_plugins );
25803
25804 $thank_you = sprintf(
25805 "<span data-plugin-id='%d'>%s</span>",
25806 $single_parent_product->id,
25807 sprintf(
25808 $single_parent_product->has_addons ?
25809 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25810 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25811 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25812 )
25813 );
25814
25815 $already_opted_in = sprintf(
25816 $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' ),
25817 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25818 );
25819 } else {
25820 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25821 $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' );
25822
25823 $products_and_add_ons = '';
25824 foreach ( $user_plugins as $user_plugin ) {
25825 if ( ! empty( $products_and_add_ons ) ) {
25826 $products_and_add_ons .= ', ';
25827 }
25828
25829 if ( ! $user_plugin->has_addons ) {
25830 $products_and_add_ons .= sprintf(
25831 "<span data-plugin-id='%d'>%s</span>",
25832 $user_plugin->id,
25833 $user_plugin->title
25834 );
25835 } else {
25836 $products_and_add_ons .= sprintf(
25837 "<span data-plugin-id='%d'>%s</span>",
25838 $user_plugin->id,
25839 sprintf(
25840 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25841 $user_plugin->title
25842 )
25843 );
25844 }
25845 }
25846
25847 $multiple_products_text = sprintf(
25848 "<small class='products'><strong>%s:</strong> %s</small>",
25849 $this->get_text_inline( 'Products', 'products' ),
25850 $products_and_add_ons
25851 );
25852 }
25853
25854 $actions = sprintf(
25855 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25856 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25857 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25858 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25859 sprintf(
25860 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25861 '<span class="underlined">',
25862 '</span>'
25863 )
25864 );
25865
25866 return sprintf(
25867 '%s %s %s',
25868 $thank_you,
25869 $already_opted_in,
25870 sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '<a href="https://ec.europa.eu/info/law/law-topic/data-protection_en/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
25871 '<br><br>' .
25872 '<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>' .
25873 $actions .
25874 ( $is_single_parent_product ? '' : $multiple_products_text )
25875 );
25876 }
25877
25878 /**
25879 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25880 * plugins and themes they've opted in to.
25881 *
25882 * @author Leo Fajardo (@leorw)
25883 * @since 2.1.0
25884 *
25885 * @param string $user_email
25886 * @param string $license_key
25887 * @param array $plugin_ids
25888 * @param string|null $license_key
25889 *
25890 * @return array|false
25891 */
25892 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25893 $request = array(
25894 'method' => 'POST',
25895 'body' => array(),
25896 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25897 );
25898
25899 if ( is_string( $user_email ) ) {
25900 $request['body']['email'] = $user_email;
25901 } else {
25902 $request['body']['license_key'] = $license_key;
25903 }
25904
25905 $result = array();
25906
25907 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25908 $total_plugin_ids = count( $plugin_ids );
25909
25910 $plugin_ids_count_per_request = 10;
25911 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25912 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25913
25914 $request['body']['plugin_ids'] = $plugin_ids_set;
25915
25916 $response = self::safe_remote_post(
25917 $url,
25918 $request,
25919 WP_FS__TIME_24_HOURS_IN_SEC,
25920 WP_FS__TIME_12_HOURS_IN_SEC
25921 );
25922
25923 if ( ! is_wp_error( $response ) ) {
25924 $decoded = is_string( $response['body'] ) ?
25925 json_decode( $response['body'] ) :
25926 null;
25927
25928 if (
25929 !is_object($decoded) ||
25930 !isset($decoded->success) ||
25931 true !== $decoded->success ||
25932 !isset( $decoded->data ) ||
25933 !is_array( $decoded->data )
25934 ) {
25935 return false;
25936 }
25937
25938 $result = array_merge( $result, $decoded->data );
25939 }
25940 }
25941
25942 return $result;
25943 }
25944
25945 /**
25946 * @author Leo Fajardo (@leorw)
25947 * @since 2.1.0
25948 */
25949 function _maybe_show_gdpr_admin_notice() {
25950 if ( ! $this->is_user_in_admin() ) {
25951 return;
25952 }
25953
25954 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25955 return;
25956 }
25957
25958 if ( ! $this->is_user_admin() ) {
25959 return;
25960 }
25961
25962 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25963
25964 $lock = FS_User_Lock::instance();
25965
25966 /**
25967 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25968 */
25969 if ( ! $lock->try_lock( 60 ) ) {
25970 return;
25971 }
25972
25973 /**
25974 * @var $current_wp_user WP_User
25975 */
25976 $current_wp_user = self::_get_current_wp_user();
25977
25978 /**
25979 * @var FS_User $current_fs_user
25980 */
25981 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
25982
25983 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
25984
25985 if ( ! is_object( $current_fs_user ) ) {
25986 // 10-year lock.
25987 $lock->lock( $ten_years_in_sec );
25988
25989 return;
25990 }
25991
25992 $gdpr = FS_GDPR_Manager::instance();
25993
25994 if ( $gdpr->is_opt_in_notice_shown() ) {
25995 // 30-day lock.
25996 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25997
25998 return;
25999 }
26000
26001 if ( ! $gdpr->should_show_opt_in_notice() ) {
26002 // 10-year lock.
26003 $lock->lock( $ten_years_in_sec );
26004
26005 return;
26006 }
26007
26008 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
26009 $was_notice_shown_before = ( false !== $last_time_notice_shown );
26010
26011 if ( $was_notice_shown_before &&
26012 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26013 ) {
26014 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26015 return;
26016 }
26017
26018 /**
26019 * Find all plugin IDs that were installed by the current admin.
26020 */
26021 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26022
26023 if ( empty( $plugin_ids_map )) {
26024 $lock->lock( $ten_years_in_sec );
26025
26026 return;
26027 }
26028
26029 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26030 $current_fs_user->email,
26031 null,
26032 array_keys( $plugin_ids_map )
26033 );
26034
26035 if ( empty( $user_plugins ) ) {
26036 $lock->lock(
26037 is_array($user_plugins) ?
26038 $ten_years_in_sec :
26039 // Lock for 24-hours on errors.
26040 WP_FS__TIME_24_HOURS_IN_SEC
26041 );
26042
26043 return;
26044 }
26045
26046 $has_unset_marketing_optin = false;
26047
26048 foreach ( $user_plugins as $user_plugin ) {
26049 if ( true == $user_plugin->is_marketing_allowed ) {
26050 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26051 }
26052
26053 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26054 $has_unset_marketing_optin = true;
26055 }
26056 }
26057
26058 if ( empty( $plugin_ids_map ) ||
26059 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26060 ) {
26061 $lock->lock( $ten_years_in_sec );
26062
26063 return;
26064 }
26065
26066 $modules = array_merge(
26067 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26068 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26069 );
26070
26071 foreach ( $modules as $module ) {
26072 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26073 $plugin_ids_map[ $module->id ] = $module;
26074 }
26075 }
26076
26077 $plugin_title = null;
26078 if ( 1 === count( $plugin_ids_map ) ) {
26079 $module = reset( $plugin_ids_map );
26080 $plugin_title = $module->title;
26081 }
26082
26083 $gdpr->add_opt_in_sticky_notice(
26084 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26085 $plugin_title
26086 );
26087
26088 $this->add_gdpr_optin_ajax_handler_and_style();
26089
26090 $gdpr->notice_was_just_shown();
26091
26092 // 30-day lock.
26093 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26094 }
26095
26096 /**
26097 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26098 * marketing.
26099 *
26100 * @author Leo Fajardo (@leorw)
26101 * @since 2.1.0
26102 */
26103 private function disable_opt_in_notice_and_lock_user() {
26104 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26105
26106 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26107
26108 // 10-year lock.
26109 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26110 }
26111
26112 /**
26113 * @author Leo Fajardo (@leorw)
26114 * @since 2.5.4
26115 */
26116 static function _add_api_connectivity_notice_handler_js() {
26117 fs_require_once_template( 'api-connectivity-message-js.php' );
26118 }
26119
26120 /**
26121 * @author Leo Fajardo (@leorw)
26122 * @since 2.1.0
26123 */
26124 function _add_gdpr_optin_js() {
26125 $vars = array( 'id' => $this->_module_id );
26126
26127 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26128 }
26129
26130 /**
26131 * @author Leo Fajardo (@leorw)
26132 * @since 2.1.0
26133 */
26134 function enqueue_gdpr_optin_notice_style() {
26135 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26136 }
26137
26138 /**
26139 * @author Leo Fajardo (@leorw)
26140 * @since 2.1.0
26141 */
26142 function _maybe_add_gdpr_optin_ajax_handler() {
26143 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26144
26145 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26146 $this->add_gdpr_optin_ajax_handler_and_style();
26147 }
26148 }
26149
26150 /**
26151 * @author Leo Fajardo (@leorw)
26152 * @since 2.1.0
26153 */
26154 function _fetch_is_marketing_required_flag_value_ajax_action() {
26155 $this->_logger->entrance();
26156
26157 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26158
26159 $license_key = fs_request_get_raw( 'license_key' );
26160
26161 if ( empty($license_key) ) {
26162 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26163 }
26164
26165 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26166 null,
26167 $license_key,
26168 array( $this->_module_id )
26169 );
26170
26171 if ( ! is_array( $user_plugins ) ||
26172 empty($user_plugins) ||
26173 !isset($user_plugins[0]->plugin_id) ||
26174 $user_plugins[0]->plugin_id != $this->_module_id
26175 ) {
26176 /**
26177 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26178 *
26179 * @author Vova Feldman (@svovaf)
26180 */
26181 self::shoot_ajax_success( array(
26182 'is_marketing_allowed' => null,
26183 'license_owner_id' => null
26184 ) );
26185 }
26186
26187 self::shoot_ajax_success( array(
26188 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26189 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26190 ) );
26191 }
26192
26193 /**
26194 * @author Leo Fajardo (@leorw)
26195 * @since 2.3.2
26196 *
26197 * @param number[] $install_ids
26198 *
26199 * @return array {
26200 * An array of objects containing the installs' licenses owners data.
26201 *
26202 * @property number $id User ID.
26203 * @property string $email User email (can be masked email).
26204 * }
26205 */
26206 private function fetch_installs_licenses_owners_data( $install_ids ) {
26207 $this->_logger->entrance();
26208
26209 $response = $this->get_api_user_scope()->get(
26210 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26211 );
26212
26213 $license_owners = array();
26214
26215 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26216 $license_owners = $response->owners;
26217 }
26218
26219 return $license_owners;
26220 }
26221
26222 /**
26223 * @author Leo Fajardo (@leorw)
26224 * @since 2.1.0
26225 */
26226 private function add_gdpr_optin_ajax_handler_and_style() {
26227 // Add GDPR action AJAX callback.
26228 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26229
26230 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26231 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26232 }
26233
26234 /**
26235 * @author Leo Fajardo (@leorw)
26236 * @since 2.1.0
26237 */
26238 function _gdpr_optin_ajax_action() {
26239 $this->_logger->entrance();
26240
26241 $this->check_ajax_referer( 'gdpr_optin_action' );
26242
26243 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26244 self::shoot_ajax_failure();
26245 }
26246
26247 $current_wp_user = self::_get_current_wp_user();
26248
26249 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26250 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26251 self::shoot_ajax_failure();
26252 }
26253
26254 $modules = array_merge(
26255 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26256 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26257 );
26258
26259 foreach ( $modules as $key => $module ) {
26260 if ( ! in_array( $module->id, $plugin_ids ) ) {
26261 unset( $modules[ $key ] );
26262 }
26263 }
26264
26265 if ( empty( $modules ) ) {
26266 self::shoot_ajax_failure();
26267 }
26268
26269 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26270
26271 foreach ( $modules as $module ) {
26272 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26273 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26274 ) );
26275 }
26276
26277 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26278
26279 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26280
26281 // 10-year lock.
26282 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26283
26284 self::shoot_ajax_success();
26285 }
26286
26287 /**
26288 * 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.
26289 *
26290 * @author Vova Feldman (@svovaf)
26291 * @since 2.1.0
26292 *
26293 * @return bool
26294 */
26295 private function should_handle_gdpr_admin_notice() {
26296 return $this->apply_filters(
26297 'handle_gdpr_admin_notice',
26298 // Default to false.
26299 false
26300 );
26301 }
26302
26303 #endregion
26304
26305 #----------------------------------------------------------------------------------
26306 #region Marketing
26307 #----------------------------------------------------------------------------------
26308
26309 /**
26310 * Check if current user purchased any other plugins before.
26311 *
26312 * @author Vova Feldman (@svovaf)
26313 * @since 1.0.9
26314 *
26315 * @return bool
26316 */
26317 function has_purchased_before() {
26318 // TODO: Implement has_purchased_before() method.
26319 throw new Exception( 'not implemented' );
26320 }
26321
26322 /**
26323 * Check if current user classified as an agency.
26324 *
26325 * @author Vova Feldman (@svovaf)
26326 * @since 1.0.9
26327 *
26328 * @return bool
26329 */
26330 function is_agency() {
26331 // TODO: Implement is_agency() method.
26332 throw new Exception( 'not implemented' );
26333 }
26334
26335 /**
26336 * Check if current user classified as a developer.
26337 *
26338 * @author Vova Feldman (@svovaf)
26339 * @since 1.0.9
26340 *
26341 * @return bool
26342 */
26343 function is_developer() {
26344 // TODO: Implement is_developer() method.
26345 throw new Exception( 'not implemented' );
26346 }
26347
26348 /**
26349 * Check if current user classified as a business.
26350 *
26351 * @author Vova Feldman (@svovaf)
26352 * @since 1.0.9
26353 *
26354 * @return bool
26355 */
26356 function is_business() {
26357 // TODO: Implement is_business() method.
26358 throw new Exception( 'not implemented' );
26359 }
26360
26361 #endregion
26362
26363 #----------------------------------------------------------------------------------
26364 #region Helper
26365 #----------------------------------------------------------------------------------
26366
26367 /**
26368 * If running with a secret key, assume it's the developer and show pending plans as well.
26369 *
26370 * @author Vova Feldman (@svovaf)
26371 * @since 2.1.2
26372 *
26373 * @param string $path
26374 *
26375 * @return string
26376 */
26377 function add_show_pending( $path ) {
26378 if ( ! $this->has_secret_key() ) {
26379 return $path;
26380 }
26381
26382 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26383 }
26384
26385 #endregion
26386 }
26387