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

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

25,365 lines 956.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 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 /**
381 * @author Leo Fajardo (@leorw)
382 * @since 2.3.1
383 *
384 * @var boolean|null
385 */
386 private $_use_external_pricing = null;
387 /**
388 * @author Leo Fajardo (@leorw)
389 * @since 2.4.2
390 *
391 * @var string|null
392 */
393 private $_pricing_js_path = null;
394
395 #endregion
396
397 /* Ctor
398 ------------------------------------------------------------------------------------------------------------------*/
399
400 /**
401 * Main singleton instance.
402 *
403 * @author Vova Feldman (@svovaf)
404 * @since 1.0.0
405 *
406 * @param number $module_id
407 * @param string|bool $slug
408 * @param bool $is_init Since 1.2.1 Is initiation sequence.
409 */
410 private function __construct( $module_id, $slug = false, $is_init = false ) {
411 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
412 $this->store_id_slug_type_path_map( $module_id, $slug );
413 }
414
415 $this->_module_id = $module_id;
416 $this->_slug = $this->get_slug();
417 $this->_module_type = $this->get_module_type();
418
419 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
420
421 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
422
423 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
424
425 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
426
427 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init );
428 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
429 $this->_plugin_basename = $this->get_plugin_basename();
430 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
431
432 $this->_is_multisite_integrated = (
433 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
434 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
435 );
436
437 $this->_is_network_active = (
438 is_multisite() &&
439 $this->_is_multisite_integrated &&
440 // Themes are always network activated, but the ACTUAL activation is per site.
441 $this->is_plugin() &&
442 (
443 is_plugin_active_for_network( $this->_plugin_basename ) ||
444 // Plugin network level activation or uninstall.
445 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
446 )
447 );
448
449 $this->_storage->set_network_active(
450 $this->_is_network_active,
451 $this->is_delegated_connection()
452 );
453
454 if ( ! isset( $this->_storage->is_network_activated ) ) {
455 $this->_storage->is_network_activated = $this->_is_network_active;
456 }
457
458 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
459 // Update last activation level.
460 $this->_storage->is_network_activated = $this->_is_network_active;
461
462 $this->maybe_adjust_storage();
463 }
464
465 #region Migration
466
467 if ( is_multisite() ) {
468 /**
469 * If the install_timestamp exists on the site level but doesn't exist on the
470 * network level storage, it means that we need to process the storage with migration.
471 *
472 * 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.
473 *
474 * @author Vova Feldman (@svovaf)
475 * @since 2.0.0
476 */
477 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
478 false !== $this->_storage->get( 'install_timestamp', false, false )
479 ) {
480 // Initiate storage migration.
481 $this->_storage->migrate_to_network();
482
483 // Migrate module cache to network level storage.
484 $this->_cache->migrate_to_network();
485 }
486 }
487
488 #endregion
489
490 $base_name_split = explode( '/', $this->_plugin_basename );
491 $this->_plugin_dir_name = $base_name_split[0];
492
493 if ( $this->_logger->is_on() ) {
494 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
495 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
496 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
497 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
498 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
499 }
500
501 // Remember link between file to slug.
502 $this->store_file_slug_map();
503
504 // Store plugin's initial install timestamp.
505 if ( ! isset( $this->_storage->install_timestamp ) ) {
506 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
507 }
508
509 if ( ! is_object( $this->_plugin ) ) {
510 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
511 }
512
513 $this->_admin_notices = FS_Admin_Notices::instance(
514 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
515 /**
516 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
517 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
518 *
519 * @author Leo Fajardo (@leorw)
520 * @since 1.2.2
521 */
522 ( is_object( $this->_plugin ) ? $this->_plugin->title : $this->get_plugin_name() ),
523 $this->get_unique_affix()
524 );
525
526 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
527 fs_request_is_action( 'restart_freemius' )
528 ) {
529 FS_Api::clear_cache();
530 $this->_cache->clear();
531 }
532
533 $this->register_constructor_hooks();
534
535 /**
536 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
537 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
538 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
539 *
540 * @author Leo Fajardo (@leorw)
541 */
542 self::migrate_install_plan_to_plan_id( $this->_storage );
543
544 $this->_load_account();
545
546 $this->_version_updates_handler();
547 }
548
549 /**
550 * @author Leo Fajardo (@leorw)
551 * @since 2.3.0
552 */
553 private function maybe_adjust_storage() {
554 $install_timestamp = null;
555 $prev_is_premium = null;
556
557 $options_to_update = array();
558
559 $is_network_admin = fs_is_network_admin();
560
561 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
562
563 if ( ! $is_network_admin ) {
564 if ( is_null( $network_install_timestamp ) ) {
565 // Plugin was not network-activated before.
566 return;
567 }
568
569 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
570 // Set the `install_timestamp` only if it's not yet set.
571 $install_timestamp = $network_install_timestamp;
572 }
573
574 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
575 } else {
576 $current_wp_user = self::_get_current_wp_user();
577 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
578 $network_user_info = array();
579
580 $skips_count = 0;
581
582 $sites = self::get_sites();
583 $sites_count = count( $sites );
584
585 $blog_id_2_install_map = array();
586
587 $is_first_non_ignored_blog = true;
588
589 foreach ( $sites as $site ) {
590 $blog_id = self::get_site_blog_id( $site );
591
592 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
593
594 if ( is_null( $blog_install_timestamp ) ) {
595 // Plugin has not been installed on this blog.
596 continue;
597 }
598
599 $is_earlier_install = (
600 ! is_null( $install_timestamp ) &&
601 $blog_install_timestamp < $install_timestamp
602 );
603
604 $install = $this->get_install_by_blog_id( $blog_id );
605
606 $update_network_user_info = false;
607
608 if ( ! is_object( $install ) ) {
609 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
610 // The opt-in decision (whether to skip or opt in) is yet to be made.
611 continue;
612 }
613
614 $skips_count ++;
615 } else {
616 $blog_id_2_install_map[ $blog_id ] = $install;
617
618 if ( empty( $network_user_info ) ) {
619 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
620 $update_network_user_info = true;
621 }
622
623 if ( ! $update_network_user_info &&
624 is_object( $current_fs_user ) &&
625 $network_user_info['user_id'] != $current_fs_user->id &&
626 $install->user_id == $current_fs_user->id
627 ) {
628 // If an install that is owned by the current WP user is found, use its user information instead.
629 $update_network_user_info = true;
630 }
631
632 if ( ! $update_network_user_info &&
633 $is_earlier_install &&
634 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
635 ) {
636 // 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.
637 $update_network_user_info = true;
638 }
639 }
640
641 if ( $update_network_user_info ) {
642 $network_user_info = array(
643 'user_id' => $install->user_id,
644 'blog_id' => $blog_id
645 );
646 }
647
648 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
649
650 if ( $is_first_non_ignored_blog ) {
651 $prev_is_premium = $site_prev_is_premium;
652
653 if ( is_null( $network_install_timestamp ) ) {
654 $install_timestamp = $blog_install_timestamp;
655 }
656
657 $is_first_non_ignored_blog = false;
658
659 continue;
660 }
661
662 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
663 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
664 $prev_is_premium = null;
665 }
666
667 if ( $is_earlier_install ) {
668 // If an earlier install timestamp is found.
669 $install_timestamp = $blog_install_timestamp;
670 }
671 }
672
673 $installs_count = count( $blog_id_2_install_map );
674
675 if ( $sites_count === ( $installs_count + $skips_count ) ) {
676 if ( ! empty( $network_user_info ) ) {
677 $options_to_update['network_user_id'] = $network_user_info['user_id'];
678 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
679
680 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
681 if ( $install->user_id == $network_user_info['user_id'] ) {
682 continue;
683 }
684
685 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
686 }
687 }
688
689 if ( $sites_count === $skips_count ) {
690 /**
691 * Assume network-level skipping as the intended action if all actions identified were only
692 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
693 */
694 $options_to_update['is_anonymous_ms'] = true;
695 } else if ( $sites_count === $installs_count ) {
696 /**
697 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
698 * (i.e., no delegation and skipping of the connections so far).
699 */
700 $options_to_update['is_network_connected'] = true;
701 }
702 }
703 }
704
705 if ( ! is_null( $install_timestamp ) ) {
706 $options_to_update['install_timestamp'] = $install_timestamp;
707 }
708
709 if ( ! is_null( $prev_is_premium ) ) {
710 $options_to_update['prev_is_premium'] = $prev_is_premium;
711 }
712
713 if ( ! empty( $options_to_update ) ) {
714 $this->adjust_storage( $options_to_update, $is_network_admin );
715 }
716 }
717
718 /**
719 * @author Leo Fajardo (@leorw)
720 * @since 2.3.0
721 *
722 * @param array $options
723 * @param bool $is_network_admin
724 */
725 private function adjust_storage( $options, $is_network_admin ) {
726 foreach ( $options as $name => $value ) {
727 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
728 }
729 }
730
731 /**
732 * Checks whether this module has a settings menu.
733 *
734 * @author Leo Fajardo (@leorw)
735 * @since 1.2.2
736 *
737 * @return bool
738 */
739 function has_settings_menu() {
740 return ( $this->_is_network_active && fs_is_network_admin() ) ?
741 $this->_menu->has_network_menu() :
742 $this->_menu->has_menu();
743 }
744
745 /**
746 * 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.
747 *
748 * @author Vova Feldman (@svovaf)
749 * @since 2.4.5
750 *
751 * @return bool
752 */
753 function show_opt_in_on_themes_page() {
754 if ( ! $this->is_free_wp_org_theme() ) {
755 return false;
756 }
757
758 if ( ! $this->has_settings_menu() ) {
759 return true;
760 }
761
762 return $this->show_settings_with_tabs();
763 }
764
765 /**
766 * If `true` the opt-in should be shown on the product's main setting page.
767 *
768 * @author Vova Feldman (@svovaf)
769 * @since 2.4.5
770 *
771 * @return bool
772 *
773 * @uses show_opt_in_on_themes_page();
774 */
775 function show_opt_in_on_setting_page() {
776 return ! $this->show_opt_in_on_themes_page();
777 }
778
779 /**
780 * If `true` the settings should be shown using tabs.
781 *
782 * @author Vova Feldman (@svovaf)
783 * @since 2.4.5
784 *
785 * @return bool
786 */
787 function show_settings_with_tabs() {
788 return ( self::NAVIGATION_TABS === $this->_navigation );
789 }
790
791 /**
792 * Check if the context module is free wp.org theme.
793 *
794 * This method is helpful because:
795 * 1. wp.org themes are limited to a single submenu item,
796 * and sub-submenu items are most likely not allowed (never verified).
797 * 2. wp.org themes are not allowed to redirect the user
798 * after the theme activation, therefore, the agreed UX
799 * is showing the opt-in as a modal dialog box after
800 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
801 *
802 * @author Vova Feldman (@svovaf)
803 * @since 1.2.2.7
804 *
805 * @return bool
806 */
807 function is_free_wp_org_theme() {
808 return (
809 $this->is_theme() &&
810 $this->is_org_repo_compliant() &&
811 ! $this->is_premium()
812 );
813 }
814
815 /**
816 * Checks whether this a submenu item is visible.
817 *
818 * @author Vova Feldman (@svovaf)
819 * @since 1.2.2.6
820 * @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.
821 *
822 * @param string $slug
823 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
824 *
825 * @return bool
826 */
827 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
828 if ( $this->is_admin_page( $slug ) ) {
829 /**
830 * It is the current context page, so show the submenu item
831 * so the user will have the right context page, even if it
832 * was set to hidden.
833 */
834 return true;
835 }
836
837 if ( ! $this->has_settings_menu() ) {
838 // No menu settings at all.
839 return false;
840 }
841
842 if (
843 ! $is_tabs_visibility_check &&
844 $this->is_org_repo_compliant() &&
845 $this->show_settings_with_tabs()
846 ) {
847 /**
848 * wp.org themes are limited to a single submenu item, and
849 * sub-submenu items are most likely not allowed (never verified).
850 */
851 return false;
852 }
853
854 return $this->_menu->is_submenu_item_visible( $slug );
855 }
856
857 /**
858 * Check if a Freemius page should be accessible via the UI.
859 *
860 * @author Vova Feldman (@svovaf)
861 * @since 1.2.2.7
862 *
863 * @param string $slug
864 *
865 * @return bool
866 */
867 function is_page_visible( $slug ) {
868 if ( $this->is_admin_page( $slug ) ) {
869 return true;
870 }
871
872 return $this->_menu->is_submenu_item_visible( $slug, true, true );
873 }
874
875 /**
876 * @author Vova Feldman (@svovaf)
877 * @since 1.0.9
878 */
879 private function _version_updates_handler() {
880 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
881 // Freemius version upgrade mode.
882 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
883 $this->_storage->sdk_version = $this->version;
884
885 if ( empty( $this->_storage->sdk_last_version ) ||
886 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
887 ) {
888 $this->_storage->sdk_upgrade_mode = true;
889 $this->_storage->sdk_downgrade_mode = false;
890 } else {
891 $this->_storage->sdk_downgrade_mode = true;
892 $this->_storage->sdk_upgrade_mode = false;
893
894 }
895
896 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
897 }
898
899 $plugin_version = $this->get_plugin_version();
900 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
901 // Plugin version upgrade mode.
902 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
903 $this->_storage->plugin_version = $plugin_version;
904
905 if ( empty( $this->_storage->plugin_last_version ) ||
906 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
907 ) {
908 $this->_storage->plugin_upgrade_mode = true;
909 $this->_storage->plugin_downgrade_mode = false;
910 } else {
911 $this->_storage->plugin_downgrade_mode = true;
912 $this->_storage->plugin_upgrade_mode = false;
913 }
914
915 if ( ! empty( $this->_storage->plugin_last_version ) ) {
916 // Different version of the plugin was installed before, therefore it's an update.
917 $this->_storage->is_plugin_new_install = false;
918 }
919
920 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
921 }
922 }
923
924 #--------------------------------------------------------------------------------
925 #region Data Migration on SDK Update
926 #--------------------------------------------------------------------------------
927
928 /**
929 * @author Vova Feldman (@svovaf)
930 * @since 1.1.5
931 *
932 * @param string $sdk_prev_version
933 * @param string $sdk_version
934 */
935 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
936 /**
937 * @since 1.1.7.3 Fixed unwanted connectivity test cleanup.
938 */
939 if ( empty( $sdk_prev_version ) ) {
940 return;
941 }
942
943 if ( version_compare( $sdk_prev_version, '2.1.0', '<' ) &&
944 version_compare( $sdk_version, '2.1.0', '>=' )
945 ) {
946 $this->_storage->handle_gdpr_admin_notice = true;
947 }
948
949 if ( version_compare( $sdk_prev_version, '2.0.0', '<' ) &&
950 version_compare( $sdk_version, '2.0.0', '>=' )
951 ) {
952 $this->migrate_to_subscriptions_collection();
953
954 $this->consolidate_licenses();
955
956 // Clear trial_plan since it's now loaded from the plans collection when needed.
957 $this->_storage->remove( 'trial_plan', true, false );
958 }
959
960 if ( version_compare( $sdk_prev_version, '1.2.3', '<' ) &&
961 version_compare( $sdk_version, '1.2.3', '>=' )
962 ) {
963 /**
964 * Starting from version 1.2.3, paths are stored as relative instead of absolute and some of them can be
965 * invalid.
966 *
967 * @author Leo Fajardo (@leorw)
968 */
969 $this->remove_invalid_paths();
970 }
971
972 if ( version_compare( $sdk_prev_version, '1.1.5', '<' ) &&
973 version_compare( $sdk_version, '1.1.5', '>=' )
974 ) {
975 // On version 1.1.5 merged connectivity and is_on data.
976 if ( isset( $this->_storage->connectivity_test ) ) {
977 if ( ! isset( $this->_storage->is_on ) ) {
978 unset( $this->_storage->connectivity_test );
979 } else {
980 $connectivity_data = $this->_storage->connectivity_test;
981 $connectivity_data['is_active'] = $this->_storage->is_on['is_active'];
982 $connectivity_data['timestamp'] = $this->_storage->is_on['timestamp'];
983
984 // Override.
985 $this->_storage->connectivity_test = $connectivity_data;
986
987 // Remove previous structure.
988 unset( $this->_storage->is_on );
989 }
990
991 }
992 }
993
994 if (
995 version_compare( $sdk_prev_version, '2.2.1', '<' ) &&
996 version_compare( $sdk_version, '2.2.1', '>=' )
997 ) {
998 /**
999 * Clear the file cache without storing the previous path since it could be a wrong path. For example,
1000 * in the versions of the SDK lower than 2.2.1, it's possible for the path of an add-on to be the same
1001 * as the parent plugin's when the add-on was auto-installed since the relevant method names were not
1002 * skipped in the logic that determines the right path in the `get_caller_main_file_and_type` method
1003 * (e.g. `try_activate_plugin`). Since it was an auto-installation, the caller was the parent plugin
1004 * and so its path was used. In case the stored path is wrong, clearing the cache will resolve issues
1005 * related to data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1006 *
1007 * @author Leo Fajardo (@leorw)
1008 * @since 2.2.1
1009 */
1010 $this->clear_module_main_file_cache( false );
1011 }
1012 }
1013
1014 /**
1015 * @author Leo Fajardo (@leorw)
1016 * @since 2.0.0
1017 *
1018 * @param \FS_Storage $storage
1019 * @param bool|int|null $blog_id
1020 */
1021 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
1022 if ( empty( $storage->sdk_version ) ) {
1023 // New installation of the plugin, no need to upgrade.
1024 return;
1025 }
1026
1027 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
1028 // Previous version is >= 2.0.0, so no need to migrate.
1029 return;
1030 }
1031
1032 // Alias.
1033 $module_type = $storage->get_module_type();
1034 $module_slug = $storage->get_module_slug();
1035
1036 $installs = self::get_all_sites( $module_type, $blog_id );
1037 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
1038
1039 if ( ! is_object( $install ) ) {
1040 return;
1041 }
1042
1043 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
1044 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
1045 $install->plan_id = self::_decrypt( $install->plan->id );
1046 }
1047
1048 unset( $install->plan );
1049
1050 $installs[ $module_slug ] = clone $install;
1051
1052 self::set_account_option_by_module(
1053 $module_type,
1054 'sites',
1055 $installs,
1056 true,
1057 $blog_id
1058 );
1059 }
1060 }
1061
1062 /**
1063 * @author Leo Fajardo (@leorw)
1064 * @since 2.0.0
1065 */
1066 private function migrate_to_subscriptions_collection() {
1067 if ( ! is_object( $this->_site ) ) {
1068 return;
1069 }
1070
1071 if ( isset( $this->_storage->subscription ) && is_object( $this->_storage->subscription ) ) {
1072 $this->_storage->subscriptions = array( fs_get_entity( $this->_storage->subscription, FS_Subscription::get_class_name() ) );
1073 }
1074 }
1075
1076 /**
1077 * @author Leo Fajardo (@leorw)
1078 * @since 2.0.0
1079 */
1080 private function consolidate_licenses() {
1081 $plugin_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_PLUGIN );
1082 if ( isset( $plugin_licenses[ $this->_slug ] ) ) {
1083 $plugin_licenses = $plugin_licenses[ $this->_slug ];
1084 } else {
1085 $plugin_licenses = array();
1086 }
1087
1088 $theme_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_THEME );
1089 if ( isset( $theme_licenses[ $this->_slug ] ) ) {
1090 $theme_licenses = $theme_licenses[ $this->_slug ];
1091 } else {
1092 $theme_licenses = array();
1093 }
1094
1095 if ( empty( $plugin_licenses ) && empty( $theme_licenses ) ) {
1096 return;
1097 }
1098
1099 $all_licenses = array();
1100 $user_id_license_ids_map = array();
1101
1102 foreach ( $plugin_licenses as $user_id => $user_licenses ) {
1103 if ( is_array( $user_licenses ) ) {
1104 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
1105 $user_id_license_ids_map[ $user_id ] = array();
1106 }
1107
1108 foreach ( $user_licenses as $user_license ) {
1109 $all_licenses[] = $user_license;
1110 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
1111 }
1112 }
1113 }
1114
1115 foreach ( $theme_licenses as $user_id => $user_licenses ) {
1116 if ( is_array( $user_licenses ) ) {
1117 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
1118 $user_id_license_ids_map[ $user_id ] = array();
1119 }
1120
1121 foreach ( $user_licenses as $user_license ) {
1122 $all_licenses[] = $user_license;
1123 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
1124 }
1125 }
1126 }
1127
1128 self::store_user_id_license_ids_map(
1129 $user_id_license_ids_map,
1130 $this->_module_id
1131 );
1132
1133 $this->_store_licenses( true, $this->_module_id, $all_licenses );
1134 }
1135
1136 /**
1137 * Remove invalid paths.
1138 *
1139 * @author Leo Fajardo (@leorw)
1140 * @since 1.2.3
1141 */
1142 private function remove_invalid_paths() {
1143 // Remove invalid path that is still associated with the current slug if there's any.
1144 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
1145 foreach ( $file_slug_map as $plugin_basename => $slug ) {
1146 if ( $slug === $this->_slug &&
1147 $plugin_basename !== $this->_plugin_basename &&
1148 ! file_exists( $this->get_absolute_path( $plugin_basename ) )
1149 ) {
1150 unset( $file_slug_map[ $plugin_basename ] );
1151 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
1152
1153 break;
1154 }
1155 }
1156 }
1157
1158 /**
1159 * @author Vova Feldman (@svovaf)
1160 * @since 1.2.2.7
1161 *
1162 * @param string $plugin_prev_version
1163 * @param string $plugin_version
1164 */
1165 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1166 if ( $this->is_theme() ) {
1167 // Expire the cache of the previous tabs since the theme may
1168 // have setting updates.
1169 $this->_cache->expire( 'tabs' );
1170 $this->_cache->expire( 'tabs_stylesheets' );
1171 }
1172 }
1173
1174 /**
1175 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1176 * - Moves some data to the network level storage.
1177 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1178 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1179 * - 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.
1180 * - 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.
1181 * - 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.
1182 *
1183 * @author Vova Feldman (@svovaf)
1184 * @since 2.0.0
1185 */
1186 private static function migrate_accounts_to_network() {
1187 $sites = self::get_sites();
1188 $sites_count = count( $sites );
1189 $connection_status = array();
1190 $plugin_slugs = array();
1191 foreach ( $sites as $site ) {
1192 $blog_id = self::get_site_blog_id( $site );
1193
1194 self::$_accounts->migrate_to_network( $blog_id );
1195
1196 /**
1197 * Build a list of all Freemius powered plugins slugs.
1198 */
1199 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1200 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1201 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1202 $plugin_slugs[ $data['slug'] ] = true;
1203 }
1204 }
1205
1206 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1207
1208 if ( is_array( $installs ) ) {
1209 foreach ( $installs as $slug => $install ) {
1210 if ( ! isset( $connection_status[ $slug ] ) ) {
1211 $connection_status[ $slug ] = array();
1212 }
1213
1214 if ( is_object( $install ) &&
1215 FS_Site::is_valid_id( $install->id ) &&
1216 FS_User::is_valid_id( $install->user_id )
1217 ) {
1218 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1219 }
1220 }
1221 }
1222 }
1223
1224 foreach ( $plugin_slugs as $slug => $true ) {
1225 if ( ! isset( $connection_status[ $slug ] ) ) {
1226 $connection_status[ $slug ] = array();
1227 }
1228
1229 foreach ( $sites as $site ) {
1230 $blog_id = self::get_site_blog_id( $site );
1231
1232 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1233 continue;
1234 }
1235
1236 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1237
1238 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1239
1240 if ( ! is_null( $is_anonymous ) ) {
1241 // Since 1.1.3 is_anonymous is an array.
1242 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1243 $is_anonymous = $is_anonymous['is'];
1244 }
1245
1246 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1247 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1248 }
1249 }
1250
1251 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1252 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1253 }
1254 }
1255 }
1256
1257 $super_admins = array();
1258
1259 foreach ( $connection_status as $slug => $blogs_status ) {
1260 $skips = 0;
1261 $ignores = 0;
1262 $connections = 0;
1263 $opted_in_users = array();
1264 $opted_in_super_admins = array();
1265
1266 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1267
1268 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1269 if ( 'skipped' === $status_or_user_id ) {
1270 $skips ++;
1271 } else if ( 'ignored' === $status_or_user_id ) {
1272 $ignores ++;
1273 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1274 $connections ++;
1275
1276 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1277 $opted_in_users[ $status_or_user_id ] = array();
1278 }
1279
1280 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1281
1282 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1283 self::is_super_admin( $status_or_user_id )
1284 ) {
1285 // Cache super-admin data.
1286 $super_admins[ $status_or_user_id ] = true;
1287
1288 // Remember opted-in super-admins for the plugin.
1289 $opted_in_super_admins[ $status_or_user_id ] = true;
1290 }
1291 }
1292 }
1293
1294 $main_super_admin_user_id = null;
1295 $all_migrated = false;
1296 if ( $sites_count == $skips ) {
1297 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1298 $storage->is_anonymous_ms = $storage->is_anonymous;
1299
1300 $all_migrated = true;
1301 } else if ( $sites_count == $ignores ) {
1302 // Don't do anything, still in activation mode.
1303
1304 $all_migrated = true;
1305 } else if ( 0 < count( $opted_in_super_admins ) ) {
1306 // Find the super-admin with the majority of installs.
1307 $max_installs_by_super_admin = 0;
1308 foreach ( $opted_in_super_admins as $user_id => $true ) {
1309 $installs_count = count( $opted_in_users[ $user_id ] );
1310
1311 if ( $installs_count > $max_installs_by_super_admin ) {
1312 $max_installs_by_super_admin = $installs_count;
1313 $main_super_admin_user_id = $user_id;
1314 }
1315 }
1316
1317 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1318 // Super-admin opted-in for all sites in the network.
1319 $storage->is_network_connected = true;
1320
1321 $all_migrated = true;
1322 }
1323
1324 // Store network user.
1325 $storage->network_user_id = $main_super_admin_user_id;
1326
1327 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1328 // Since all sites are opted-in, associating with the main site.
1329 get_current_blog_id() :
1330 // Associating with the 1st found opted-in site.
1331 $opted_in_users[ $main_super_admin_user_id ][0];
1332
1333 /**
1334 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1335 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1336 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1337 * will be empty.
1338 */
1339 $storage->migrate_to_network();
1340 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1341 } else {
1342 // At least one opt-in. All the opt-in were created by a non-super-admin.
1343 if ( 0 == $ignores ) {
1344 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1345 $storage->store( 'is_delegated_connection', true, true );
1346
1347 $all_migrated = true;
1348 }
1349 }
1350
1351 if ( ! $all_migrated ) {
1352 /**
1353 * Delegate all sites that were:
1354 * 1) Opted-in by a user that is NOT the main-super-admin.
1355 * 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.
1356 */
1357 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1358 if ( $status_or_user_id == $main_super_admin_user_id ) {
1359 continue;
1360 }
1361
1362 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1363 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1364 ) {
1365 $storage->store( 'is_delegated_connection', true, $blog_id );
1366 }
1367 }
1368 }
1369
1370
1371 if ( ( $connections + $skips > 0 ) ) {
1372 if ( $ignores > 0 ) {
1373 /**
1374 * If admin already opted-in or skipped in any of the network sites, and also
1375 * have sites which the connection decision was not yet taken, set this plugin
1376 * into network activation mode so the super-admin can choose what to do with
1377 * the rest of the sites.
1378 */
1379 self::set_network_upgrade_mode( $storage );
1380 }
1381 }
1382 }
1383 }
1384
1385 /**
1386 * Set a module into network upgrade mode.
1387 *
1388 * @author Vova Feldman (@svovaf)
1389 * @since 2.0.0
1390 *
1391 * @param \FS_Storage $storage
1392 *
1393 * @return bool
1394 */
1395 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1396 return $storage->is_network_activation = true;
1397 }
1398
1399 /**
1400 * Will return true after upgrading to the SDK with the network level integration,
1401 * when the super-admin involvement is required regarding the rest of the sites.
1402 *
1403 * @author Vova Feldman (@svovaf)
1404 * @since 2.0.0
1405 *
1406 * @return bool
1407 */
1408 function is_network_upgrade_mode() {
1409 return $this->_storage->get( 'is_network_activation' );
1410 }
1411
1412 /**
1413 * Clear flag after the upgrade mode completion.
1414 *
1415 * @author Vova Feldman (@svovaf)
1416 * @since 2.0.0
1417 *
1418 * @return bool True if network activation was on and now completed.
1419 */
1420 private function network_upgrade_mode_completed() {
1421 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1422 $this->_storage->remove( 'is_network_activation' );
1423
1424 return true;
1425 }
1426
1427 return false;
1428 }
1429
1430 #endregion
1431
1432 /**
1433 * This action is connected to the 'plugins_loaded' hook and helps to determine
1434 * if this is a new plugin installation or a plugin update.
1435 *
1436 * There are 3 different use-cases:
1437 * 1) New plugin installation right with Freemius:
1438 * 1.1 _activate_plugin_event_hook() will be executed first
1439 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1440 * and $this->_storage->plugin_last_version is not set,
1441 * $this->_storage->is_plugin_new_install will be set to TRUE.
1442 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1443 * be already set to TRUE.
1444 *
1445 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1446 * 2.1 _activate_plugin_event_hook() will not be executed, because
1447 * the activation hook do NOT fires on updates since WP 3.1.
1448 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1449 * be empty, therefore, it will be set to FALSE.
1450 *
1451 * 3) Plugin update, had Freemius in prev version as well:
1452 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1453 * before, $this->_storage->plugin_last_version will NOT be empty,
1454 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1455 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1456 * already set, therefore, it will not be modified.
1457 *
1458 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1459 *
1460 * NOTE:
1461 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1462 * and then, the next immediate PageView is the plugin's main settings page, it will not
1463 * show the opt-in right away. The reason it will happen is because Freemius execution
1464 * will be turned off till the plugin is fully loaded at least once
1465 * (till $this->_storage->was_plugin_loaded is TRUE).
1466 *
1467 * @author Vova Feldman (@svovaf)
1468 * @since 1.1.9
1469 *
1470 */
1471 function _plugins_loaded() {
1472 // Update flag that plugin was loaded with Freemius at least once.
1473 $this->_storage->was_plugin_loaded = true;
1474
1475 /**
1476 * Bug fix - only set to false when it's a plugin, due to the
1477 * execution sequence of the theme hooks and our methods, if
1478 * this will be set for themes, Freemius will always assume
1479 * it's a theme update.
1480 *
1481 * @author Vova Feldman (@svovaf)
1482 * @since 1.2.2.2
1483 */
1484 if ( $this->is_plugin() &&
1485 ! isset( $this->_storage->is_plugin_new_install )
1486 ) {
1487 $this->_storage->is_plugin_new_install = (
1488 ! is_plugin_active( $this->_plugin_basename ) &&
1489 empty( $this->_storage->plugin_last_version )
1490 );
1491 }
1492 }
1493
1494 /**
1495 * Add special parameter to WP admin AJAX calls so when we
1496 * process AJAX calls we can identify its source properly.
1497 *
1498 * @author Leo Fajardo (@leorw)
1499 * @since 2.0.0
1500 */
1501 static function _enrich_ajax_url() {
1502 $admin_param = is_network_admin() ?
1503 '_fs_network_admin' :
1504 '_fs_blog_admin';
1505 ?>
1506 <script type="text/javascript">
1507 (function ($) {
1508 $(document).ajaxSend(function (event, jqxhr, settings) {
1509 if (settings.url &&
1510 -1 < settings.url.indexOf('admin-ajax.php') &&
1511 ! ( settings.url.indexOf( '<?php echo $admin_param ?>' ) > 0 )
1512 ) {
1513 if (
1514 'string' === typeof settings.data &&
1515 settings.data.indexOf( 'action=heartbeat' ) > 0
1516 ) {
1517 return;
1518 }
1519
1520 if (settings.url.indexOf('?') > 0) {
1521 settings.url += '&';
1522 } else {
1523 settings.url += '?';
1524 }
1525
1526 settings.url += '<?php echo $admin_param ?>=true';
1527 }
1528 });
1529 })(jQuery);
1530 </script>
1531 <?php
1532 }
1533
1534 /**
1535 * Opens the support forum subemenu item in a new browser page.
1536 *
1537 * @author Vova Feldman (@svovaf)
1538 * @since 2.1.4
1539 */
1540 static function _open_support_forum_in_new_page() {
1541 ?>
1542 <script type="text/javascript">
1543 (function ($) {
1544 $('.fs-submenu-item.wp-support-forum').parent().attr( { target: '_blank', rel: 'noopener noreferrer' } );
1545 })(jQuery);
1546 </script>
1547 <?php
1548 }
1549
1550 /**
1551 * @author Vova Feldman (@svovaf)
1552 * @since 1.0.9
1553 */
1554 private function register_constructor_hooks() {
1555 $this->_logger->entrance();
1556
1557 if ( is_admin() ) {
1558 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1559
1560 if ( $this->is_plugin() ) {
1561 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1562 /**
1563 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1564 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1565 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1566 * updating of a .org plugin).
1567 */
1568 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1569 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1570 /**
1571 * 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.
1572 *
1573 * @author Leo Fajardo (@leorw)
1574 * @since 2.2.3
1575 */
1576 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1577 }
1578
1579 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1580
1581 /**
1582 * @since 1.2.2
1583 *
1584 * Hook to both free and premium version activations to support
1585 * auto deactivation on the other version activation.
1586 */
1587 register_activation_hook(
1588 $plugin_dir . $this->_free_plugin_basename,
1589 array( &$this, '_activate_plugin_event_hook' )
1590 );
1591
1592 register_activation_hook(
1593 $plugin_dir . $this->premium_plugin_basename(),
1594 array( &$this, '_activate_plugin_event_hook' )
1595 );
1596 } else {
1597 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1598
1599 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1600 }
1601
1602 /**
1603 * Part of the mechanism to identify new plugin install vs. plugin update.
1604 *
1605 * @author Vova Feldman (@svovaf)
1606 * @since 1.1.9
1607 */
1608 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1609 /**
1610 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1611 * when the logic gets here since the activation logic first add the activate plugins,
1612 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1613 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1614 * plugin activation, and that IS intentional.
1615 *
1616 * @author Vova Feldman (@svovaf)
1617 */
1618 if ( $this->is_plugin() &&
1619 $this->is_activation_mode( false ) &&
1620 0 == did_action( 'plugins_loaded' )
1621 ) {
1622 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1623 } else {
1624 // If was activated before, then it was already loaded before.
1625 $this->_plugins_loaded();
1626 }
1627 }
1628
1629 if ( ! self::is_ajax() ) {
1630 if ( ! $this->is_addon() ) {
1631 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1632 }
1633 }
1634
1635 if ( $this->_storage->handle_gdpr_admin_notice ) {
1636 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1637 }
1638
1639 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1640 add_action( 'init', array( &$this, '_maybe_add_pricing_ajax_handler' ) );
1641 }
1642
1643 if ( $this->is_plugin() ) {
1644 if ( $this->_is_network_active ) {
1645 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1646 }
1647
1648 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1649 }
1650
1651 if ( is_multisite() ) {
1652 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1653 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1654 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1655 add_action( 'deleted_blog', array( &$this, '_after_site_deleted_callback' ), 10, 2 );
1656
1657 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1658 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1659 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1660 }
1661
1662 if ( $this->is_theme() &&
1663 self::is_customizer() &&
1664 $this->apply_filters( 'show_customizer_upsell', true )
1665 ) {
1666 // Register customizer upsell.
1667 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1668 }
1669
1670 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1671
1672 if ( $this->is_theme() && ! $this->is_migration() ) {
1673 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1674 }
1675
1676 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1677 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1678 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1679 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1680
1681 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1682 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1683 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1684 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1685
1686 if ( $this->_is_network_active && fs_is_network_admin() ) {
1687 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1688 }
1689
1690 $this->add_ajax_action( 'install_premium_version', array(
1691 &$this,
1692 '_install_premium_version_ajax_action'
1693 ) );
1694
1695 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1696
1697 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1698
1699 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1700
1701 $this->add_action(
1702 'plugin_version_update',
1703 array( &$this, '_after_version_update' ),
1704 WP_FS__DEFAULT_PRIORITY,
1705 2
1706 );
1707 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1708
1709 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
1710 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
1711 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1712
1713 /**
1714 * Handle request to reset anonymous mode for `get_reconnect_url()`.
1715 *
1716 * @author Vova Feldman (@svovaf)
1717 * @since 1.2.1.5
1718 */
1719 if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
1720 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
1721 ) {
1722 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1723 }
1724 }
1725
1726 /**
1727 * Register the required hooks right after the settings parse is completed.
1728 *
1729 * @author Vova Feldman (@svovaf)
1730 * @since 2.3.1
1731 */
1732 private function register_after_settings_parse_hooks() {
1733 if ( is_admin() &&
1734 $this->is_theme() &&
1735 $this->is_premium() &&
1736 ! $this->has_active_valid_license()
1737 ) {
1738 $this->add_ajax_action(
1739 'delete_theme_update_data',
1740 array( &$this, '_delete_theme_update_data_action' )
1741 );
1742 }
1743
1744 if ( $this->show_settings_with_tabs() ) {
1745 /**
1746 * Include the required hooks to capture the theme settings' page tabs
1747 * and cache them.
1748 *
1749 * @author Vova Feldman (@svovaf)
1750 * @since 1.2.2.7
1751 */
1752 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1753 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1754 // Add license activation AJAX callback.
1755 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1756
1757 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1758 }
1759
1760 add_action(
1761 'admin_footer',
1762 array( &$this, '_add_freemius_tabs' ),
1763 /**
1764 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1765 * That's why the priority is 11 while the tabs capture logic is added
1766 * with priority 10.
1767 *
1768 * @author Vova Feldman (@svovaf)
1769 */
1770 11
1771 );
1772 }
1773
1774 if ( ! self::is_ajax() ) {
1775 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1776 add_action(
1777 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1778 array( &$this, '_prepare_admin_menu' ),
1779 WP_FS__LOWEST_PRIORITY
1780 );
1781 }
1782 }
1783 }
1784
1785 /**
1786 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1787 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1788 *
1789 * @author Leo Fajardo (@leorw)
1790 * @since 2.2.3
1791 *
1792 * @param object $updates
1793 * @param string|null $transient
1794 *
1795 * @return object
1796 */
1797 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1798 if ( is_object( $updates ) && isset( $updates->response ) ) {
1799 foreach ( $updates->response as $file => $plugin ) {
1800 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1801 unset( $updates->response[ $file ] );
1802 }
1803 }
1804 }
1805
1806 return $updates;
1807 }
1808
1809 /**
1810 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1811 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1812 *
1813 * @author Leo Fajardo (@leorw)
1814 * @since 2.2.3
1815 *
1816 * @return string
1817 */
1818 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1819 $slug_basename_map = array();
1820 foreach ( self::$_instances as $instance ) {
1821 if ( ! $instance->is_plugin() ) {
1822 continue;
1823 }
1824
1825 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1826 }
1827 ?>
1828 <script type="text/javascript">
1829 (function( $ ) {
1830 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1831 for ( var slug in slugBasenameMap ) {
1832 var basename = slugBasenameMap[ slug ];
1833
1834 // Try to get the plugin rows if on the "Plugins" page.
1835 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1836
1837 if ( 0 === $pluginRows.length ) {
1838 // Try to get the plugin rows if on the "Updates" page.
1839 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1840 if ( 0 !== $pluginCheckbox.length ) {
1841 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1842 }
1843 }
1844
1845 if ( 0 === $pluginRows.length ) {
1846 // No plugin rows found.
1847 continue;
1848 }
1849
1850 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1851 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1852 var $this = $( this ),
1853 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1854
1855 $this.attr( 'href', href );
1856 });
1857 }
1858 })( jQuery );
1859 </script>
1860 <?php
1861 }
1862
1863 /**
1864 * @author Leo Fajardo (@leorw)
1865 * @since 2.3.0
1866 */
1867 static function _maybe_add_beta_label_styles() {
1868 $has_any_beta_version = false;
1869
1870 foreach ( self::$_instances as $instance ) {
1871 if ( $instance->is_beta() ) {
1872 $has_any_beta_version = true;
1873 break;
1874 }
1875 }
1876
1877 if ( $has_any_beta_version ) {
1878 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1879 }
1880 }
1881
1882 /**
1883 * @author Leo Fajardo (@leorw)
1884 * @since 2.3.0
1885 */
1886 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1887 $beta_data = array();
1888
1889 foreach ( self::$_instances as $instance ) {
1890 if ( ! $instance->is_premium() ) {
1891 continue;
1892 }
1893
1894 /**
1895 * If there's an available beta version update, a confirmation message will be shown when the
1896 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1897 */
1898 $has_beta_update = $instance->has_beta_update();
1899
1900 $is_beta = (
1901 // The "Beta" label is added separately for themes.
1902 $instance->is_plugin() &&
1903 $instance->is_beta()
1904 );
1905
1906 if ( ! $is_beta && ! $has_beta_update ) {
1907 continue;
1908 }
1909
1910 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1911
1912 if ( ! $has_beta_update ) {
1913 continue;
1914 }
1915
1916 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1917 '%s %s',
1918 sprintf(
1919 fs_esc_attr_inline(
1920 '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.',
1921 'beta-version-update-caution',
1922 $instance->get_slug()
1923 ),
1924 $instance->get_plugin_title()
1925 ),
1926 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1927 );
1928 }
1929
1930 if ( empty( $beta_data ) ) {
1931 return;
1932 }
1933 ?>
1934 <script type="text/javascript">
1935 ( function( $ ) {
1936 var betaData = <?php echo json_encode( $beta_data ) ?>;
1937
1938 for ( var pluginBasename in betaData ) {
1939 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1940 continue;
1941 }
1942
1943 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1944 continue;
1945 }
1946
1947 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1948 if ( 0 === $parentContainer.length ) {
1949 continue;
1950 }
1951
1952 $parentContainer.find( '.plugin-title > strong:first-child').append(
1953 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1954 );
1955 }
1956
1957 setTimeout( function() {
1958 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1959 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1960 var $parentContainer = $( this ).parents( 'tr:first' );
1961 pluginBasename = ( 0 !== $parentContainer.length ) ?
1962 $parentContainer.data( 'plugin' ) :
1963 $( this ).parents( '.theme:first' ).data( 'slug' );
1964
1965 if (
1966 betaData[ pluginBasename ] &&
1967 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1968 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1969 ) {
1970 return false;
1971 }
1972 } );
1973 }, 20 );
1974 } )( jQuery );
1975 </script>
1976 <?php
1977 }
1978
1979 /**
1980 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1981 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1982 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1983 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1984 *
1985 * @author Leo Fajardo (@leorw)
1986 *
1987 * @since 1.2.0
1988 */
1989 private function unregister_uninstall_hook() {
1990 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1991 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1992 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1993
1994 update_option( 'uninstall_plugins', $uninstallable_plugins );
1995 }
1996
1997 /**
1998 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1999 *
2000 * @param bool $store_prev_path
2001 */
2002 private function clear_module_main_file_cache( $store_prev_path = true ) {
2003 if ( ! isset( $this->_storage->plugin_main_file ) ||
2004 empty( $this->_storage->plugin_main_file->path )
2005 ) {
2006 return;
2007 }
2008
2009 if ( ! $store_prev_path ) {
2010 /**
2011 * Storing the previous path is not needed when clearing the cache after an SDK version update since
2012 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
2013 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
2014 *
2015 * @author Leo Fajardo (@leorw)
2016 * @since 2.2.1
2017 */
2018 unset( $this->_storage->plugin_main_file->path );
2019 } else {
2020 $plugin_main_file = clone $this->_storage->plugin_main_file;
2021
2022 // Store cached path (2nd layer cache).
2023 $plugin_main_file->prev_path = $plugin_main_file->path;
2024
2025 // Clear cached path.
2026 unset( $plugin_main_file->path );
2027
2028 $this->_storage->plugin_main_file = $plugin_main_file;
2029 }
2030
2031 /**
2032 * Clear global cached path.
2033 *
2034 * @author Leo Fajardo (@leorw)
2035 * @since 1.2.2
2036 */
2037 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
2038 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
2039 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2040 }
2041
2042 /**
2043 * @author Leo Fajardo (@leorw)
2044 * @since 2.0.0
2045 */
2046 function _hook_action_links_and_register_account_hooks() {
2047 if ( $this->is_migration() ) {
2048 return;
2049 }
2050
2051 $this->_add_tracking_links();
2052
2053 if ( self::is_plugins_page() && $this->is_plugin() ) {
2054 $this->hook_plugin_action_links();
2055 }
2056
2057 $this->_register_account_hooks();
2058 }
2059
2060 /**
2061 * @author Vova Feldman (@svovaf)
2062 * @since 1.0.9
2063 */
2064 private function _register_account_hooks() {
2065 if ( ! is_admin() ) {
2066 return;
2067 }
2068
2069 /**
2070 * Always show the deactivation feedback form since we added
2071 * automatic free version deactivation upon premium code activation.
2072 *
2073 * @since 1.2.1.6
2074 */
2075 $this->add_ajax_action(
2076 'submit_uninstall_reason',
2077 array( &$this, '_submit_uninstall_reason_action' )
2078 );
2079
2080 $this->add_ajax_action(
2081 'cancel_subscription_or_trial',
2082 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
2083 );
2084
2085 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
2086 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
2087 ( $this->is_theme() && self::is_themes_page() )
2088 ) {
2089 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
2090 }
2091 }
2092 }
2093
2094 /**
2095 * Leverage backtrace to find caller plugin file path.
2096 *
2097 * @author Vova Feldman (@svovaf)
2098 * @since 1.0.6
2099 *
2100 * @param bool $is_init Is initiation sequence.
2101 *
2102 * @return string
2103 */
2104 private function _find_caller_plugin_file( $is_init = false ) {
2105 // Try to load the cached value of the file path.
2106 if ( isset( $this->_storage->plugin_main_file ) ) {
2107 $plugin_main_file = $this->_storage->plugin_main_file;
2108 if ( ! empty( $plugin_main_file->path ) ) {
2109 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
2110 if ( file_exists( $absolute_path ) ) {
2111 return $absolute_path;
2112 }
2113 }
2114 }
2115
2116 /**
2117 * @since 1.2.1
2118 *
2119 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
2120 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
2121 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
2122 *
2123 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
2124 * when the class instantiator isn't the module.
2125 */
2126 if ( ! $is_init ) {
2127 // Fetch prev path cache.
2128 if ( isset( $this->_storage->plugin_main_file ) &&
2129 ! empty( $this->_storage->plugin_main_file->prev_path )
2130 ) {
2131 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2132 if ( file_exists( $absolute_path ) ) {
2133 return $absolute_path;
2134 }
2135 }
2136
2137 wp_die(
2138 $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' ) .
2139 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2140 $this->get_text_inline( 'Error', 'error' ),
2141 array( 'back_link' => true )
2142 );
2143 }
2144
2145 /**
2146 * @since 1.2.1
2147 *
2148 * Only the original instantiator that calls dynamic_init can modify the module's path.
2149 */
2150 // Find caller module.
2151 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2152 $this->_storage->plugin_main_file = (object) array(
2153 'path' => $id_slug_type_path_map[ $this->_module_id ]['path'],
2154 );
2155
2156 return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
2157 }
2158
2159 /**
2160 * @author Leo Fajardo (@leorw)
2161 * @since 1.2.3
2162 *
2163 * @param string $path
2164 *
2165 * @return string
2166 */
2167 private function get_relative_path( $path ) {
2168 $module_root_dir = $this->get_module_root_dir_path();
2169 if ( 0 === strpos( $path, $module_root_dir ) ) {
2170 $path = substr( $path, strlen( $module_root_dir ) );
2171 }
2172
2173 return $path;
2174 }
2175
2176 /**
2177 * @author Leo Fajardo (@leorw)
2178 * @since 1.2.3
2179 *
2180 * @param string $path
2181 * @param string|bool $module_type
2182 *
2183 * @return string
2184 */
2185 private function get_absolute_path( $path, $module_type = false ) {
2186 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2187 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2188 $path = fs_normalize_path( $module_root_dir . $path );
2189 }
2190
2191 return $path;
2192 }
2193
2194 /**
2195 * @author Leo Fajardo (@leorw)
2196 * @since 1.2.3
2197 *
2198 * @param string|bool $module_type
2199 *
2200 * @return string
2201 */
2202 private function get_module_root_dir_path( $module_type = false ) {
2203 $is_plugin = empty( $module_type ) ?
2204 $this->is_plugin() :
2205 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2206
2207 return fs_normalize_path( trailingslashit( $is_plugin ?
2208 WP_PLUGIN_DIR :
2209 get_theme_root( get_stylesheet() ) ) );
2210 }
2211
2212 /**
2213 * @author Leo Fajardo (@leorw)
2214 *
2215 * @param number $module_id
2216 * @param string $slug
2217 *
2218 * @since 1.2.2
2219 */
2220 private function store_id_slug_type_path_map( $module_id, $slug ) {
2221 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2222
2223 $store_option = false;
2224
2225 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2226 $id_slug_type_path_map[ $module_id ] = array(
2227 'slug' => $slug
2228 );
2229
2230 $store_option = true;
2231 } else if (
2232 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2233 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2234 ) {
2235 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2236 $store_option = true;
2237 }
2238
2239 if ( empty( $id_slug_type_path_map[ $module_id ]['path'] ) ||
2240 /**
2241 * This verification is for cases when suddenly the same module
2242 * is installed but with a different folder name.
2243 *
2244 * @author Vova Feldman (@svovaf)
2245 * @since 1.2.3
2246 */
2247 ! file_exists( $this->get_absolute_path(
2248 $id_slug_type_path_map[ $module_id ]['path'],
2249 $id_slug_type_path_map[ $module_id ]['type']
2250 ) )
2251 ) {
2252 $caller_main_file_and_type = $this->get_caller_main_file_and_type();
2253
2254 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2255 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2256
2257 $store_option = true;
2258 }
2259
2260 if ( $store_option ) {
2261 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2262 }
2263 }
2264
2265 /**
2266 * Identifies the caller type: plugin or theme.
2267 *
2268 * @author Leo Fajardo (@leorw)
2269 * @since 1.2.2
2270 *
2271 * @author Vova Feldman (@svovaf)
2272 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2273 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2274 * SDK an internal file instead of directly from functions.php.
2275 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2276 */
2277 private function get_caller_main_file_and_type() {
2278 self::require_plugin_essentials();
2279
2280 $all_plugins = fs_get_plugins( true );
2281 $all_plugins_paths = array();
2282
2283 // Get active plugin's main files real full names (might be symlinks).
2284 foreach ( $all_plugins as $relative_path => $data ) {
2285 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2286 /**
2287 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2288 * can't really include the SDK.
2289 *
2290 * @author Vova Feldman
2291 * @since 1.2.1.7
2292 */
2293 continue;
2294 }
2295
2296 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2297 }
2298
2299 $caller_file_candidate = false;
2300 $caller_map = array();
2301 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2302 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2303 $plugin_dir_to_skip = false;
2304
2305 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2306 if ( empty( $bt[ $i ]['file'] ) ) {
2307 continue;
2308 }
2309
2310 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2311 // If file same as the prev file in the stack, skip it.
2312 continue;
2313 }
2314
2315 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2316 'do_action',
2317 'apply_filter',
2318 // The string split is stupid, but otherwise, theme check
2319 // throws info notices.
2320 'requir' . 'e_once',
2321 'requir' . 'e',
2322 'includ' . 'e_once',
2323 'includ' . 'e',
2324 'install_and_activate_plugin',
2325 'try_activate_plugin',
2326 'activate_plugin'
2327 ) )
2328 ) {
2329 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2330 /**
2331 * Store the directory of the activator plugin so that any other file that starts with it
2332 * cannot be mistakenly chosen as a candidate caller file.
2333 *
2334 * @author Leo Fajardo
2335 *
2336 * @since 2.3.0
2337 */
2338 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2339
2340 foreach ( $all_plugins_paths as $plugin_path ) {
2341 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2342 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2343 $plugin_dir_to_skip = $plugin_dir;
2344
2345 break;
2346 }
2347 }
2348 }
2349
2350 // Ignore call stack hooks and files inclusion.
2351 continue;
2352 }
2353
2354 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2355
2356 if ( ! empty( $plugin_dir_to_skip ) ) {
2357 /**
2358 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2359 *
2360 * @author Leo Fajardo
2361 *
2362 * @since 2.3.0
2363 */
2364 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2365 continue;
2366 }
2367 }
2368
2369 if ( 'functions.php' === basename( $caller_file_path ) ) {
2370 /**
2371 * 1. Assumes that theme's starting execution file is functions.php.
2372 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2373 *
2374 * @author Vova Feldman (@svovaf)
2375 * @since 1.2.2.5
2376 */
2377
2378 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2379 $module_type = WP_FS__MODULE_TYPE_THEME;
2380
2381 /**
2382 * Relative path of the theme, e.g.:
2383 * `my-theme/functions.php`
2384 *
2385 * @author Leo Fajardo (@leorw)
2386 */
2387 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2388 '/' .
2389 basename( $caller_file_path );
2390
2391 continue;
2392 }
2393 }
2394
2395 $caller_file_hash = md5( $caller_file_path );
2396
2397 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2398 foreach ( $all_plugins_paths as $plugin_path ) {
2399 if ( empty( $plugin_path ) ) {
2400 continue;
2401 }
2402
2403 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2404 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2405 break;
2406 }
2407 }
2408 }
2409
2410 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2411 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2412 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2413 }
2414 }
2415
2416 return (object) array(
2417 'module_type' => $module_type,
2418 'path' => $caller_file_candidate
2419 );
2420 }
2421
2422 #----------------------------------------------------------------------------------
2423 #region Deactivation Feedback Form
2424 #----------------------------------------------------------------------------------
2425
2426 /**
2427 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2428 * page.
2429 *
2430 * @author Vova Feldman (@svovaf)
2431 * @author Leo Fajardo (@leorw)
2432 *
2433 * @since 1.1.2
2434 */
2435 function _add_deactivation_feedback_dialog_box() {
2436 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2437 $this->_get_subscription_cancellation_dialog_box_template_params() :
2438 array();
2439
2440 /**
2441 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2442 */
2443 $show_deactivation_feedback_form = true;
2444 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2445 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2446 } else if ( $this->is_addon() ) {
2447 /**
2448 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2449 */
2450 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2451 }
2452
2453 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2454
2455 if (
2456 empty( $subscription_cancellation_dialog_box_template_params ) &&
2457 ! $show_deactivation_feedback_form &&
2458 empty( $uninstall_confirmation_message )
2459 ) {
2460 return;
2461 }
2462
2463 $vars = array( 'id' => $this->_module_id );
2464
2465 if ( $show_deactivation_feedback_form ) {
2466 /* Check the type of user:
2467 * 1. Long-term (long-term)
2468 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2469 * 3. Short-term (short-term)
2470 */
2471 $is_long_term_user = true;
2472
2473 // Check if the site is at least 2 days old.
2474 $time_installed = $this->_storage->install_timestamp;
2475
2476 // Difference in seconds.
2477 $date_diff = time() - $time_installed;
2478
2479 // Convert seconds to days.
2480 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2481
2482 if ( $date_diff_days < 2 ) {
2483 $is_long_term_user = false;
2484 }
2485
2486 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2487
2488 if ( $is_long_term_user ) {
2489 $user_type = 'long-term';
2490 } else {
2491 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2492 $user_type = 'non-registered-and-non-anonymous-short-term';
2493 } else {
2494 $user_type = 'short-term';
2495 }
2496 }
2497
2498 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2499
2500 $vars['reasons'] = $uninstall_reasons;
2501 }
2502
2503 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2504 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2505 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2506
2507 /**
2508 * Load the HTML template for the deactivation feedback dialog box.
2509 *
2510 * @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.
2511 */
2512 fs_require_template( 'forms/deactivation/form.php', $vars );
2513 }
2514
2515 /**
2516 * @author Leo Fajardo (@leorw)
2517 * @since 1.1.2
2518 *
2519 * @param string $user_type
2520 *
2521 * @return array The uninstall reasons for the specified user type.
2522 */
2523 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2524 $module_type = $this->_module_type;
2525
2526 $internal_message_template_var = array(
2527 'id' => $this->_module_id
2528 );
2529
2530 $plan = $this->get_plan();
2531
2532 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2533 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2534 } else {
2535 $contact_support_template = '';
2536 }
2537
2538 $reason_found_better_plugin = array(
2539 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2540 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2541 'input_type' => 'textfield',
2542 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2543 );
2544
2545 $reason_temporary_deactivation = array(
2546 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2547 'text' => sprintf(
2548 $this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ),
2549 strtolower( $this->is_plugin() ?
2550 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2551 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2552 )
2553 ),
2554 'input_type' => '',
2555 'input_placeholder' => ''
2556 );
2557
2558 $reason_other = array(
2559 'id' => self::REASON_OTHER,
2560 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2561 'input_type' => 'textfield',
2562 'input_placeholder' => ''
2563 );
2564
2565 $long_term_user_reasons = array(
2566 array(
2567 'id' => self::REASON_NO_LONGER_NEEDED,
2568 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2569 'input_type' => '',
2570 'input_placeholder' => ''
2571 ),
2572 $reason_found_better_plugin,
2573 array(
2574 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2575 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2576 'input_type' => '',
2577 'input_placeholder' => ''
2578 ),
2579 array(
2580 'id' => self::REASON_BROKE_MY_SITE,
2581 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2582 'input_type' => '',
2583 'input_placeholder' => '',
2584 'internal_message' => $contact_support_template
2585 ),
2586 array(
2587 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2588 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2589 'input_type' => '',
2590 'input_placeholder' => '',
2591 'internal_message' => $contact_support_template
2592 )
2593 );
2594
2595 if ( $this->is_paying() ) {
2596 $long_term_user_reasons[] = array(
2597 'id' => self::REASON_CANT_PAY_ANYMORE,
2598 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2599 'input_type' => 'textfield',
2600 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2601 );
2602 }
2603
2604 $reason_dont_share_info = array(
2605 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2606 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2607 'input_type' => '',
2608 'input_placeholder' => ''
2609 );
2610
2611 /**
2612 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2613 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2614 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2615 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2616 * button in the opt-in form is shown/hidden).
2617 */
2618 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2619 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2620 }
2621
2622 $uninstall_reasons = array(
2623 'long-term' => $long_term_user_reasons,
2624 'non-registered-and-non-anonymous-short-term' => array(
2625 array(
2626 'id' => self::REASON_DIDNT_WORK,
2627 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2628 'input_type' => '',
2629 'input_placeholder' => ''
2630 ),
2631 $reason_dont_share_info,
2632 $reason_found_better_plugin
2633 ),
2634 'short-term' => array(
2635 array(
2636 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2637 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2638 'input_type' => '',
2639 'input_placeholder' => '',
2640 'internal_message' => $contact_support_template
2641 ),
2642 $reason_found_better_plugin,
2643 array(
2644 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2645 '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 ),
2646 'input_type' => 'textarea',
2647 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2648 ),
2649 array(
2650 'id' => self::REASON_NOT_WORKING,
2651 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2652 'input_type' => 'textarea',
2653 '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' )
2654 ),
2655 array(
2656 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2657 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2658 'input_type' => 'textarea',
2659 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2660 ),
2661 array(
2662 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2663 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2664 'input_type' => 'textarea',
2665 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2666 )
2667 )
2668 );
2669
2670 // Randomize the reasons for the current user type.
2671 shuffle( $uninstall_reasons[ $user_type ] );
2672
2673 // Keep the following reasons as the last items in the list.
2674 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2675 $uninstall_reasons[ $user_type ][] = $reason_other;
2676
2677 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2678
2679 return $uninstall_reasons[ $user_type ];
2680 }
2681
2682 /**
2683 * Called after the user has submitted his reason for deactivating the plugin.
2684 *
2685 * @author Leo Fajardo (@leorw)
2686 * @since 1.1.2
2687 */
2688 function _submit_uninstall_reason_action() {
2689 $this->_logger->entrance();
2690
2691 $this->check_ajax_referer( 'submit_uninstall_reason' );
2692
2693 $reason_id = fs_request_get( 'reason_id' );
2694
2695 // Check if the given reason ID is an unsigned integer.
2696 if ( ! ctype_digit( $reason_id ) ) {
2697 exit;
2698 }
2699
2700 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2701 if ( ! empty( $reason_info ) ) {
2702 $reason_info = substr( $reason_info, 0, 128 );
2703 }
2704
2705 $reason = (object) array(
2706 'id' => $reason_id,
2707 'info' => $reason_info,
2708 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2709 );
2710
2711 $this->_storage->store( 'uninstall_reason', $reason );
2712
2713 /**
2714 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2715 * not support uninstall hook.
2716 *
2717 * @author Leo Fajardo (@leorw)
2718 * @since 1.2.2
2719 */
2720 if ( $this->is_theme() ) {
2721 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2722 FS_Plugin_Updater::instance( $this )->delete_update_data();
2723 }
2724
2725 $this->_uninstall_plugin_event( false );
2726 $this->remove_sdk_reference();
2727 }
2728
2729 // Print '1' for successful operation.
2730 echo 1;
2731 exit;
2732 }
2733
2734 /**
2735 * @author Leo Fajardo (@leorw)
2736 * @since 2.1.4
2737 */
2738 function cancel_subscription_or_trial_ajax_action() {
2739 $this->_logger->entrance();
2740
2741 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2742
2743 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2744
2745 if ( $this->is_api_error( $result ) ) {
2746 $this->shoot_ajax_failure( $result->error->message );
2747 }
2748
2749 $this->shoot_ajax_success();
2750 }
2751
2752 /**
2753 * @author Leo Fajardo (@leorw)
2754 * @since 2.1.4
2755 *
2756 * @param number $plugin_id
2757 *
2758 * @return object
2759 */
2760 private function cancel_subscription_or_trial( $plugin_id ) {
2761 $fs = null;
2762 if ( $plugin_id == $this->get_id() ) {
2763 $fs = $this;
2764 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2765 $fs = self::get_instance_by_id( $plugin_id );
2766 }
2767
2768 $result = null;
2769
2770 if ( ! is_null( $fs ) ) {
2771 $result = $fs->is_paid_trial() ?
2772 $fs->_cancel_trial() :
2773 $fs->_downgrade_site();
2774 }
2775
2776 return $result;
2777 }
2778
2779 /**
2780 * @author Leo Fajardo (@leorw)
2781 * @since 2.0.2
2782 */
2783 function _delete_theme_update_data_action() {
2784 FS_Plugin_Updater::instance( $this )->delete_update_data();
2785 }
2786
2787 #endregion
2788
2789 #----------------------------------------------------------------------------------
2790 #region Instance
2791 #----------------------------------------------------------------------------------
2792
2793 /**
2794 * Main singleton instance.
2795 *
2796 * @author Vova Feldman (@svovaf)
2797 * @since 1.0.0
2798 *
2799 * @param number $module_id
2800 * @param string|bool $slug
2801 * @param bool $is_init Is initiation sequence.
2802 *
2803 * @return Freemius|false
2804 */
2805 static function instance( $module_id, $slug = false, $is_init = false ) {
2806 if ( empty( $module_id ) ) {
2807 return false;
2808 }
2809
2810 /**
2811 * 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.
2812 */
2813 self::_load_required_static();
2814
2815 if ( ! is_numeric( $module_id ) ) {
2816 if ( ! $is_init && true === $slug ) {
2817 $is_init = true;
2818 }
2819
2820 $slug = $module_id;
2821
2822 $module = FS_Plugin_Manager::instance( $slug )->get();
2823
2824 if ( is_object( $module ) ) {
2825 $module_id = $module->id;
2826 }
2827 }
2828
2829 $key = 'm_' . $module_id;
2830
2831 if ( ! isset( self::$_instances[ $key ] ) ) {
2832 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2833 }
2834
2835 return self::$_instances[ $key ];
2836 }
2837
2838 /**
2839 * @author Vova Feldman (@svovaf)
2840 * @since 1.0.6
2841 *
2842 * @param number $addon_id
2843 *
2844 * @return bool
2845 */
2846 private static function has_instance( $addon_id ) {
2847 return isset( self::$_instances[ 'm_' . $addon_id ] );
2848 }
2849
2850 /**
2851 * @author Leo Fajardo (@leorw)
2852 * @since 1.2.2
2853 *
2854 * @param string|number $id_or_slug
2855 * @param string $module_type
2856 *
2857 * @return number|false
2858 */
2859 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2860 if ( is_numeric( $id_or_slug ) ) {
2861 return $id_or_slug;
2862 }
2863
2864 foreach ( self::$_instances as $instance ) {
2865 // Also check the module type since there can be a plugin and a theme with the same slug.
2866 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2867 return $instance->get_id();
2868 }
2869 }
2870
2871 return false;
2872 }
2873
2874 /**
2875 * @author Vova Feldman (@svovaf)
2876 * @since 1.0.6
2877 *
2878 * @param number $id
2879 *
2880 * @return false|Freemius
2881 */
2882 static function get_instance_by_id( $id ) {
2883 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2884 self::$_instances[ 'm_' . $id ] :
2885 false;
2886 }
2887
2888 /**
2889 *
2890 * @author Vova Feldman (@svovaf)
2891 * @since 1.0.1
2892 *
2893 * @param string $plugin_file
2894 * @param string $module_type
2895 *
2896 * @return false|Freemius
2897 */
2898 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2899 $slug = self::find_slug_by_basename( $plugin_file );
2900
2901 return ( false !== $slug ) ?
2902 self::instance( self::get_module_id( $slug, $module_type ) ) :
2903 false;
2904 }
2905
2906 /**
2907 * @author Vova Feldman (@svovaf)
2908 * @since 1.0.6
2909 *
2910 * @return false|Freemius
2911 */
2912 function get_parent_instance() {
2913 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2914 }
2915
2916 /**
2917 * @author Vova Feldman (@svovaf)
2918 * @since 1.0.6
2919 *
2920 * @param string|number $id_or_slug
2921 *
2922 * @return false|Freemius
2923 */
2924 function get_addon_instance( $id_or_slug ) {
2925 $addon_id = self::get_module_id( $id_or_slug );
2926
2927 return self::instance( $addon_id );
2928 }
2929
2930 #endregion ------------------------------------------------------------------
2931
2932 /**
2933 * @author Vova Feldman (@svovaf)
2934 * @since 1.0.6
2935 *
2936 * @return bool
2937 */
2938 function is_parent_plugin_installed() {
2939 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2940
2941 if ( $is_active ) {
2942 return true;
2943 }
2944
2945 /**
2946 * Parent module might be a theme. If that's the case, the add-on's FS
2947 * instance will be loaded prior to the theme's FS instance, therefore,
2948 * we need to check if it's active with a "look ahead".
2949 *
2950 * @author Vova Feldman
2951 * @since 1.2.2.3
2952 */
2953 global $fs_active_plugins;
2954 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2955 $active_theme = wp_get_theme();
2956
2957 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2958 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2959 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2960 // Parent module is a theme and it's currently active.
2961 return true;
2962 }
2963 }
2964 }
2965 }
2966
2967 return false;
2968 }
2969
2970 /**
2971 * Check if add-on parent plugin in activation mode.
2972 *
2973 * @author Vova Feldman (@svovaf)
2974 * @since 1.0.7
2975 *
2976 * @return bool
2977 */
2978 function is_parent_in_activation() {
2979 $parent_fs = $this->get_parent_instance();
2980 if ( ! is_object( $parent_fs ) ) {
2981 return false;
2982 }
2983
2984 return ( $parent_fs->is_activation_mode() );
2985 }
2986
2987 /**
2988 * Is plugin in activation mode.
2989 *
2990 * @author Vova Feldman (@svovaf)
2991 * @since 1.0.7
2992 *
2993 * @param bool $and_on
2994 *
2995 * @return bool
2996 */
2997 function is_activation_mode( $and_on = true ) {
2998 return fs_is_network_admin() ?
2999 $this->is_network_activation_mode( $and_on ) :
3000 $this->is_site_activation_mode( $and_on );
3001 }
3002
3003 /**
3004 * Is plugin in activation mode.
3005 *
3006 * @author Vova Feldman (@svovaf)
3007 * @since 1.0.7
3008 *
3009 * @param bool $and_on
3010 *
3011 * @return bool
3012 */
3013 function is_site_activation_mode( $and_on = true ) {
3014 return (
3015 ( $this->is_on() || ! $and_on ) &&
3016 (
3017 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
3018 (
3019 ( ! $this->is_registered() ||
3020 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
3021 ( ! $this->is_enable_anonymous() ||
3022 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
3023 )
3024 )
3025 );
3026 }
3027
3028 /**
3029 * Checks if the SDK in network activation mode.
3030 *
3031 * @author Leo Fajardo (@leorw)
3032 * @since 2.0.0
3033 *
3034 * @param bool $and_on
3035 *
3036 * @return bool
3037 */
3038 private function is_network_activation_mode( $and_on = true ) {
3039 if ( ! $this->_is_network_active ) {
3040 // Not network activated.
3041 return false;
3042 }
3043
3044 if ( $this->is_network_upgrade_mode() ) {
3045 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3046 return true;
3047 }
3048
3049 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3050 // 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.
3051 return false;
3052 }
3053
3054 if ( $this->is_network_delegated_connection() ) {
3055 // Super-admin delegated the connection to the site admins -> not activation mode.
3056 return false;
3057 }
3058
3059 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3060 // Super-admin skipped the connection network wide -> not activation mode.
3061 return false;
3062 }
3063
3064 if ( $this->is_network_registered() ) {
3065 // Super-admin connected at least one site -> not activation mode.
3066 return false;
3067 }
3068
3069 return true;
3070 }
3071
3072 /**
3073 * Check if current page is the opt-in/pending-activation page.
3074 *
3075 * @author Vova Feldman (@svovaf)
3076 * @since 1.2.1.7
3077 *
3078 * @return bool
3079 */
3080 function is_activation_page() {
3081 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3082 return true;
3083 }
3084
3085 if ( ! $this->is_activation_mode() ) {
3086 return false;
3087 }
3088
3089 // Check if current page is matching the activation page.
3090 return $this->is_matching_url( $this->get_activation_url() );
3091 }
3092
3093 /**
3094 * Check if URL path's are matching and that all querystring
3095 * arguments of the $sub_url exist in the $url with the same values.
3096 *
3097 * WARNING:
3098 * 1. This method doesn't check if the sub/domain are matching.
3099 * 2. Ignore case sensitivity.
3100 *
3101 * @author Vova Feldman (@svovaf)
3102 * @since 1.2.1.7
3103 *
3104 * @param string $sub_url
3105 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3106 *
3107 * @return bool
3108 */
3109 private function is_matching_url( $sub_url, $url = '' ) {
3110 if ( empty( $url ) ) {
3111 $url = $_SERVER['REQUEST_URI'];
3112 }
3113
3114 $url = strtolower( $url );
3115 $sub_url = strtolower( $sub_url );
3116
3117 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3118 // Different path - DO NOT OVERRIDE PAGE.
3119 return false;
3120 }
3121
3122 $url_params = array();
3123 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
3124
3125 $sub_url_params = array();
3126 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
3127
3128 foreach ( $sub_url_params as $key => $val ) {
3129 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3130 // Not matching query string - DO NOT OVERRIDE PAGE.
3131 return false;
3132 }
3133 }
3134
3135 return true;
3136 }
3137
3138 /**
3139 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3140 *
3141 * @author Vova Feldman (@svovaf)
3142 * @since 2.0.0
3143 *
3144 * @param int $blog_id
3145 *
3146 * @return string[]
3147 */
3148 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3149 if ( is_multisite() && $blog_id > 0 ) {
3150 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3151 } else {
3152 $active_basenames = get_option( 'active_plugins' );
3153 }
3154
3155 if ( ! is_array( $active_basenames ) ) {
3156 $active_basenames = array();
3157 }
3158
3159 if ( is_multisite() ) {
3160 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3161
3162 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3163 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3164 }
3165 }
3166
3167 return $active_basenames;
3168 }
3169
3170 /**
3171 * @author Leo Fajardo (@leorw)
3172 * @since 2.3.0
3173 *
3174 * @param int $blog_id
3175 *
3176 * @return array
3177 */
3178 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3179 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3180
3181 $map = array();
3182
3183 foreach ( $active_basenames as $active_basename ) {
3184 $active_basename = fs_normalize_path( $active_basename );
3185
3186 if ( false === strpos( $active_basename, '/' ) ) {
3187 continue;
3188 }
3189
3190 $map[ dirname( $active_basename ) ] = true;
3191 }
3192
3193 return $map;
3194 }
3195
3196 /**
3197 * Get collection of all active plugins. Including network activated plugins.
3198 *
3199 * @author Vova Feldman (@svovaf)
3200 * @since 1.0.9
3201 *
3202 * @param int $blog_id Since 2.0.0
3203 *
3204 * @return array[string]array
3205 */
3206 private static function get_active_plugins( $blog_id = 0 ) {
3207 self::require_plugin_essentials();
3208
3209 $active_plugin = array();
3210 $all_plugins = fs_get_plugins();
3211 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3212
3213 foreach ( $active_plugins_basenames as $plugin_basename ) {
3214 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3215 }
3216
3217 return $active_plugin;
3218 }
3219
3220 /**
3221 * Get collection of all site active plugins for a specified blog.
3222 *
3223 * @author Vova Feldman (@svovaf)
3224 * @since 2.0.0
3225 *
3226 * @param int $blog_id
3227 *
3228 * @return array[string]array
3229 */
3230 private static function get_site_active_plugins( $blog_id = 0 ) {
3231 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3232 get_blog_option( $blog_id, 'active_plugins' ) :
3233 get_option( 'active_plugins' );
3234
3235 $active = array();
3236
3237 if ( ! is_array( $active_basenames ) ) {
3238 return $active;
3239 }
3240
3241 foreach ( $active_basenames as $basename ) {
3242 $active[ $basename ] = array(
3243 'is_active' => true,
3244 'Version' => '1.0', // Dummy version.
3245 'slug' => self::get_plugin_slug( $basename ),
3246 );
3247 }
3248
3249 return $active;
3250 }
3251
3252 /**
3253 * Get collection of all plugins with their activation status for a specified blog.
3254 *
3255 * @author Vova Feldman (@svovaf)
3256 * @since 1.1.8
3257 *
3258 * @param int $blog_id Since 2.0.0
3259 *
3260 * @return array Key is the plugin file path and the value is an array of the plugin data.
3261 */
3262 private static function get_all_plugins( $blog_id = 0 ) {
3263 self::require_plugin_essentials();
3264
3265 $all_plugins = fs_get_plugins();
3266
3267 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3268
3269 foreach ( $all_plugins as $basename => &$data ) {
3270 // By default set to inactive (next foreach update the active plugins).
3271 $data['is_active'] = false;
3272 // Enrich with plugin slug.
3273 $data['slug'] = self::get_plugin_slug( $basename );
3274 }
3275
3276 // Flag active plugins.
3277 foreach ( $active_plugins_basenames as $basename ) {
3278 if ( isset( $all_plugins[ $basename ] ) ) {
3279 $all_plugins[ $basename ]['is_active'] = true;
3280 }
3281 }
3282
3283 return $all_plugins;
3284 }
3285
3286 /**
3287 * Get collection of all plugins and if they are network level activated.
3288 *
3289 * @author Vova Feldman (@svovaf)
3290 * @since 2.0.0
3291 *
3292 * @return array Key is the plugin basename and the value is an array of the plugin data.
3293 */
3294 private static function get_network_plugins() {
3295 self::require_plugin_essentials();
3296
3297 $all_plugins = fs_get_plugins();
3298
3299 $network_active_basenames = is_multisite() ?
3300 get_site_option( 'active_sitewide_plugins' ) :
3301 array();
3302
3303 foreach ( $all_plugins as $basename => &$data ) {
3304 // By default set to inactive (next foreach update the active plugins).
3305 $data['is_active'] = false;
3306 // Enrich with plugin slug.
3307 $data['slug'] = self::get_plugin_slug( $basename );
3308 }
3309
3310 // Flag active plugins.
3311 foreach ( $network_active_basenames as $basename ) {
3312 if ( isset( $all_plugins[ $basename ] ) ) {
3313 $all_plugins[ $basename ]['is_active'] = true;
3314 }
3315 }
3316
3317 return $all_plugins;
3318 }
3319
3320 /**
3321 * Cached result of get_site_transient( 'update_plugins' )
3322 *
3323 * @author Vova Feldman (@svovaf)
3324 * @since 1.1.8
3325 *
3326 * @var object
3327 */
3328 private static $_plugins_info;
3329
3330 /**
3331 * Helper function to get specified plugin's slug.
3332 *
3333 * @author Vova Feldman (@svovaf)
3334 * @since 1.1.8
3335 *
3336 * @param $basename
3337 *
3338 * @return string
3339 */
3340 private static function get_plugin_slug( $basename ) {
3341 if ( ! isset( self::$_plugins_info ) ) {
3342 self::$_plugins_info = get_site_transient( 'update_plugins' );
3343 }
3344
3345 $slug = '';
3346
3347 if ( is_object( self::$_plugins_info ) ) {
3348 if ( isset( self::$_plugins_info->no_update ) &&
3349 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3350 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3351 ) {
3352 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3353 } else if ( isset( self::$_plugins_info->response ) &&
3354 isset( self::$_plugins_info->response[ $basename ] ) &&
3355 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3356 ) {
3357 $slug = self::$_plugins_info->response[ $basename ]->slug;
3358 }
3359 }
3360
3361 if ( empty( $slug ) ) {
3362 // Try to find slug from FS data.
3363 $slug = self::find_slug_by_basename( $basename );
3364 }
3365
3366 if ( empty( $slug ) ) {
3367 // Fallback to plugin's folder name.
3368 $slug = dirname( $basename );
3369 }
3370
3371 return $slug;
3372 }
3373
3374 private static $_statics_loaded = false;
3375
3376 /**
3377 * Load static resources.
3378 *
3379 * @author Vova Feldman (@svovaf)
3380 * @since 1.0.1
3381 */
3382 private static function _load_required_static() {
3383 if ( self::$_statics_loaded ) {
3384 return;
3385 }
3386
3387 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3388
3389 self::$_static_logger->entrance();
3390
3391 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3392
3393 if ( is_multisite() ) {
3394 $has_skipped_migration = (
3395 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3396 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3397 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3398 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3399 );
3400
3401 /**
3402 * If the file_slug_map exists on the site level but doesn't exist on the
3403 * network level storage, it means that we need to process the storage with migration.
3404 *
3405 * 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.
3406 *
3407 * @author Vova Feldman (@svovaf)
3408 * @since 2.0.0
3409 */
3410 if (
3411 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3412 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3413 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3414 ) {
3415 self::migrate_options_to_network();
3416 }
3417 }
3418
3419 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3420
3421 if ( ! WP_FS__DEMO_MODE ) {
3422 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3423 'Freemius',
3424 '_add_debug_section'
3425 ) );
3426 }
3427
3428 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3429
3430 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3431
3432 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3433
3434 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3435
3436 if ( 0 == did_action( 'plugins_loaded' ) ) {
3437 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3438 }
3439
3440 add_action( 'admin_footer', array( 'Freemius', '_enrich_ajax_url' ) );
3441 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3442
3443 if ( self::is_plugins_page() || self::is_themes_page() ) {
3444 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3445
3446 /**
3447 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3448 * page.
3449 *
3450 * @author Leo Fajardo (@leorw)
3451 * @since 2.3.0
3452 */
3453 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3454 }
3455
3456 self::$_statics_loaded = true;
3457 }
3458
3459 /**
3460 * @author Leo Fajardo (@leorw)
3461 *
3462 * @since 2.1.3
3463 */
3464 private static function migrate_options_to_network() {
3465 self::migrate_accounts_to_network();
3466
3467 // Migrate API options from site level to network level.
3468 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3469 $api_network_options->migrate_to_network();
3470
3471 // Migrate API cache to network level storage.
3472 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3473
3474 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3475 }
3476
3477 #----------------------------------------------------------------------------------
3478 #region Localization
3479 #----------------------------------------------------------------------------------
3480
3481 /**
3482 * Load framework's text domain.
3483 *
3484 * @author Vova Feldman (@svovaf)
3485 * @since 1.2.1
3486 */
3487 static function _load_textdomain() {
3488 if ( ! is_admin() ) {
3489 return;
3490 }
3491
3492 global $fs_active_plugins;
3493
3494 // Works both for plugins and themes.
3495 load_plugin_textdomain(
3496 'freemius',
3497 false,
3498 $fs_active_plugins->newest->sdk_path . '/languages/'
3499 );
3500 }
3501
3502 #endregion
3503
3504 #----------------------------------------------------------------------------------
3505 #region Debugging
3506 #----------------------------------------------------------------------------------
3507
3508 /**
3509 * @author Vova Feldman (@svovaf)
3510 * @since 1.0.8
3511 */
3512 static function _add_debug_section() {
3513 if ( ! is_super_admin() ) {
3514 // Add debug page only for super-admins.
3515 return;
3516 }
3517
3518 self::$_static_logger->entrance();
3519
3520 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3521
3522 if ( WP_FS__DEV_MODE ) {
3523 // Add top-level debug menu item.
3524 $hook = FS_Admin_Menu_Manager::add_page(
3525 $title,
3526 $title,
3527 'manage_options',
3528 'freemius',
3529 array( 'Freemius', '_debug_page_render' )
3530 );
3531 } else {
3532 // Add hidden debug page.
3533 $hook = FS_Admin_Menu_Manager::add_subpage(
3534 null,
3535 $title,
3536 $title,
3537 'manage_options',
3538 'freemius',
3539 array( 'Freemius', '_debug_page_render' )
3540 );
3541 }
3542
3543 if ( ! empty( $hook ) ) {
3544 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3545 }
3546 }
3547
3548 /**
3549 * @author Vova Feldman (@svovaf)
3550 * @since 1.1.7.3
3551 */
3552 static function _toggle_debug_mode() {
3553 if ( ! is_super_admin() ) {
3554 return;
3555 }
3556
3557 $is_on = fs_request_get( 'is_on', false, 'post' );
3558
3559 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3560 update_option( 'fs_debug_mode', $is_on );
3561
3562 // Turn on/off storage logging.
3563 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3564 }
3565
3566 exit;
3567 }
3568
3569 /**
3570 * @author Vova Feldman (@svovaf)
3571 * @since 1.2.1.6
3572 */
3573 static function _get_debug_log() {
3574 $logs = FS_Logger::load_db_logs(
3575 fs_request_get( 'filters', false, 'post' ),
3576 ! empty( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? $_POST['limit'] : 200,
3577 ! empty( $_POST['offset'] ) && is_numeric( $_POST['offset'] ) ? $_POST['offset'] : 0
3578 );
3579
3580 self::shoot_ajax_success( $logs );
3581 }
3582
3583 /**
3584 * @author Vova Feldman (@svovaf)
3585 * @since 1.2.1.7
3586 */
3587 static function _get_db_option() {
3588 check_admin_referer( 'fs_get_db_option' );
3589
3590 $option_name = fs_request_get( 'option_name' );
3591
3592 if ( ! is_super_admin() ||
3593 ! fs_starts_with( $option_name, 'fs_' )
3594 ) {
3595 self::shoot_ajax_failure();
3596 }
3597
3598 $value = get_option( $option_name );
3599
3600 $result = array(
3601 'name' => $option_name,
3602 );
3603
3604 if ( false !== $value ) {
3605 if ( ! is_string( $value ) ) {
3606 $value = json_encode( $value );
3607 }
3608
3609 $result['value'] = $value;
3610 }
3611
3612 self::shoot_ajax_success( $result );
3613 }
3614
3615 /**
3616 * @author Vova Feldman (@svovaf)
3617 * @since 1.2.1.7
3618 */
3619 static function _set_db_option() {
3620 check_admin_referer( 'fs_set_db_option' );
3621
3622 $option_name = fs_request_get( 'option_name' );
3623
3624 if ( ! is_super_admin() ||
3625 ! fs_starts_with( $option_name, 'fs_' )
3626 ) {
3627 self::shoot_ajax_failure();
3628 }
3629
3630 $option_value = fs_request_get( 'option_value' );
3631
3632 if ( ! empty( $option_value ) ) {
3633 update_option( $option_name, $option_value );
3634 }
3635
3636 self::shoot_ajax_success();
3637 }
3638
3639 /**
3640 * @author Vova Feldman (@svovaf)
3641 * @since 1.0.8
3642 */
3643 static function _debug_page_actions() {
3644 self::_clean_admin_content_section();
3645
3646 if ( fs_request_is_action( 'restart_freemius' ) ) {
3647 check_admin_referer( 'restart_freemius' );
3648
3649 if ( ! is_multisite() ) {
3650 // Clear accounts data.
3651 self::$_accounts->clear( null, true );
3652 } else {
3653 $sites = self::get_sites();
3654 foreach ( $sites as $site ) {
3655 $blog_id = self::get_site_blog_id( $site );
3656 self::$_accounts->clear( $blog_id, true );
3657 }
3658
3659 // Clear network level storage.
3660 self::$_accounts->clear( true, true );
3661 }
3662
3663 // Clear SDK reference cache.
3664 delete_option( 'fs_active_plugins' );
3665 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3666 check_admin_referer( 'clear_updates_data' );
3667
3668 if ( ! is_multisite() ) {
3669 set_site_transient( 'update_plugins', null );
3670 set_site_transient( 'update_themes', null );
3671 } else {
3672 $current_blog_id = get_current_blog_id();
3673
3674 $sites = self::get_sites();
3675 foreach ( $sites as $site ) {
3676 switch_to_blog( self::get_site_blog_id( $site ) );
3677
3678 set_site_transient( 'update_plugins', null );
3679 set_site_transient( 'update_themes', null );
3680 }
3681
3682 switch_to_blog( $current_blog_id );
3683 }
3684 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3685 check_admin_referer( 'simulate_trial' );
3686
3687 $fs = freemius( fs_request_get( 'module_id' ) );
3688
3689 // Update SDK install to at least 24 hours before.
3690 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3691 // Unset the trial shown timestamp.
3692 unset( $fs->_storage->trial_promotion_shown );
3693 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3694 check_admin_referer( 'simulate_network_upgrade' );
3695
3696 $fs = freemius( fs_request_get( 'module_id' ) );
3697
3698 self::set_network_upgrade_mode( $fs->_storage );
3699 } else if ( fs_request_is_action( 'delete_install' ) ) {
3700 check_admin_referer( 'delete_install' );
3701
3702 self::_delete_site_by_slug(
3703 fs_request_get( 'slug' ),
3704 fs_request_get( 'module_type' ),
3705 true,
3706 fs_request_get( 'blog_id', null )
3707 );
3708 } else if ( fs_request_is_action( 'delete_user' ) ) {
3709 check_admin_referer( 'delete_user' );
3710
3711 self::delete_user( fs_request_get( 'user_id' ) );
3712 } else if ( fs_request_is_action( 'download_logs' ) ) {
3713 check_admin_referer( 'download_logs' );
3714
3715 $download_url = FS_Logger::download_db_logs(
3716 fs_request_get( 'filters', false, 'post' )
3717 );
3718
3719 if ( false === $download_url ) {
3720 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.' );
3721 }
3722
3723 fs_redirect( $download_url );
3724 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3725 check_admin_referer( 'migrate_options_to_network' );
3726
3727 self::migrate_options_to_network();
3728 }
3729 }
3730
3731 /**
3732 * @author Vova Feldman (@svovaf)
3733 * @since 1.0.8
3734 */
3735 static function _debug_page_render() {
3736 self::$_static_logger->entrance();
3737
3738 if ( ! is_multisite() ) {
3739 $all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
3740 $all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME );
3741 } else {
3742 $sites = self::get_sites();
3743
3744 $all_plugins_installs = array();
3745 $all_themes_installs = array();
3746
3747 foreach ( $sites as $site ) {
3748 $blog_id = self::get_site_blog_id( $site );
3749
3750 $plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
3751
3752 foreach ( $plugins_installs as $slug => $install ) {
3753 if ( ! isset( $all_plugins_installs[ $slug ] ) ) {
3754 $all_plugins_installs[ $slug ] = array();
3755 }
3756
3757 $install->blog_id = $blog_id;
3758
3759 $all_plugins_installs[ $slug ][] = $install;
3760 }
3761
3762 $themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id );
3763
3764 foreach ( $themes_installs as $slug => $install ) {
3765 if ( ! isset( $all_themes_installs[ $slug ] ) ) {
3766 $all_themes_installs[ $slug ] = array();
3767 }
3768
3769 $install->blog_id = $blog_id;
3770
3771 $all_themes_installs[ $slug ][] = $install;
3772 }
3773 }
3774 }
3775
3776 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3777
3778 $vars = array(
3779 'plugin_sites' => $all_plugins_installs,
3780 'theme_sites' => $all_themes_installs,
3781 'users' => self::get_all_users(),
3782 'addons' => self::get_all_addons(),
3783 'account_addons' => self::get_all_account_addons(),
3784 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3785 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3786 );
3787
3788 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3789 fs_require_once_template( 'debug.php', $vars );
3790 }
3791
3792 #endregion
3793
3794 #----------------------------------------------------------------------------------
3795 #region Connectivity Issues
3796 #----------------------------------------------------------------------------------
3797
3798 /**
3799 * Check if Freemius should be turned on for the current plugin install.
3800 *
3801 * Note:
3802 * $this->_is_on is updated in has_api_connectivity()
3803 *
3804 * @author Vova Feldman (@svovaf)
3805 * @since 1.0.9
3806 *
3807 * @return bool
3808 */
3809 function is_on() {
3810 self::$_static_logger->entrance();
3811
3812 if ( isset( $this->_is_on ) ) {
3813 return $this->_is_on;
3814 }
3815
3816 // If already installed or pending then sure it's on :)
3817 if ( $this->is_registered() || $this->is_pending_activation() ) {
3818 $this->_is_on = true;
3819
3820 return true;
3821 }
3822
3823 return false;
3824 }
3825
3826 /**
3827 * @author Vova Feldman (@svovaf)
3828 * @since 1.1.7.3
3829 *
3830 * @param bool $flush_if_no_connectivity
3831 *
3832 * @return bool
3833 */
3834 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3835 if ( ! isset( $this->_storage->connectivity_test ) ) {
3836 // Connectivity test was never executed, or cache was cleared.
3837 return true;
3838 }
3839
3840 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3841 if ( WP_FS__IS_HTTP_REQUEST ) {
3842 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3843 // Domain changed.
3844 return true;
3845 }
3846
3847 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3848 // Server IP changed.
3849 return true;
3850 }
3851 }
3852 }
3853
3854 if ( $this->_storage->connectivity_test['is_connected'] &&
3855 $this->_storage->connectivity_test['is_active']
3856 ) {
3857 // API connected and Freemius is active - no need to run connectivity check.
3858 return false;
3859 }
3860
3861 if ( $flush_if_no_connectivity ) {
3862 /**
3863 * If explicitly asked to flush when no connectivity - do it only
3864 * if at least 10 sec passed from the last API connectivity test.
3865 */
3866 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3867 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3868 }
3869
3870 /**
3871 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3872 */
3873 $version = $this->get_plugin_version();
3874 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3875 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3876 return true;
3877 }
3878
3879 return false;
3880 }
3881
3882 /**
3883 * @author Vova Feldman (@svovaf)
3884 * @since 1.1.7.4
3885 *
3886 * @param int|null $blog_id Since 2.0.0.
3887 * @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test.
3888 *
3889 * @return object|false
3890 */
3891 private function ping( $blog_id = null, $is_gdpr_test = false ) {
3892 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
3893 return false;
3894 }
3895
3896 $version = $this->get_plugin_version();
3897
3898 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
3899
3900 return $this->get_api_plugin_scope()->ping(
3901 $this->get_anonymous_id( $blog_id ),
3902 array(
3903 'is_update' => json_encode( $is_update ),
3904 'version' => $version,
3905 'sdk' => $this->version,
3906 'is_admin' => json_encode( is_admin() ),
3907 'is_ajax' => json_encode( self::is_ajax() ),
3908 'is_cron' => json_encode( self::is_cron() ),
3909 'is_gdpr_test' => $is_gdpr_test,
3910 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
3911 )
3912 );
3913 }
3914
3915 /**
3916 * Check if there's any connectivity issue to Freemius API.
3917 *
3918 * @author Vova Feldman (@svovaf)
3919 * @since 1.0.9
3920 *
3921 * @param bool $flush_if_no_connectivity
3922 *
3923 * @return bool
3924 */
3925 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3926 $this->_logger->entrance();
3927
3928 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3929 return $this->_has_api_connection;
3930 }
3931
3932 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3933 isset( $this->_storage->connectivity_test ) &&
3934 true === $this->_storage->connectivity_test['is_connected']
3935 ) {
3936 unset( $this->_storage->connectivity_test );
3937 }
3938
3939 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3940 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3941 /**
3942 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3943 *
3944 * @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.
3945 */
3946 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3947 $this->is_premium() ||
3948 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3949
3950 return $this->_has_api_connection;
3951 }
3952
3953 $pong = $this->ping();
3954 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3955
3956 if ( ! $is_connected ) {
3957 // API failure.
3958 $this->_add_connectivity_issue_message( $pong );
3959 }
3960
3961 if ( $is_connected ) {
3962 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
3963 }
3964
3965 $this->store_connectivity_info( $pong, $is_connected );
3966
3967 return $this->_has_api_connection;
3968 }
3969
3970 /**
3971 * @author Vova Feldman (@svovaf)
3972 * @since 1.1.7.4
3973 *
3974 * @param object $pong
3975 * @param bool $is_connected
3976 */
3977 private function store_connectivity_info( $pong, $is_connected ) {
3978 $this->_logger->entrance();
3979
3980 $version = $this->get_plugin_version();
3981
3982 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3983 $is_active = false;
3984 } else {
3985 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3986 }
3987
3988 $is_active = $this->apply_filters(
3989 'is_on',
3990 $is_active,
3991 $this->is_plugin_update(),
3992 $version
3993 );
3994
3995 $this->_storage->connectivity_test = array(
3996 'is_connected' => $is_connected,
3997 'host' => $_SERVER['HTTP_HOST'],
3998 'server_ip' => WP_FS__REMOTE_ADDR,
3999 'is_active' => $is_active,
4000 'timestamp' => WP_FS__SCRIPT_START_TIME,
4001 // Last version with connectivity attempt.
4002 'version' => $version,
4003 );
4004
4005 $this->_has_api_connection = $is_connected;
4006 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4007 }
4008
4009 /**
4010 * Force turning Freemius on.
4011 *
4012 * @author Vova Feldman (@svovaf)
4013 * @since 1.1.8.1
4014 *
4015 * @return bool TRUE if successfully turned on.
4016 */
4017 private function turn_on() {
4018 $this->_logger->entrance();
4019
4020 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4021 return false;
4022 }
4023
4024 $updated_connectivity = $this->_storage->connectivity_test;
4025 $updated_connectivity['is_active'] = true;
4026 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4027 $this->_storage->connectivity_test = $updated_connectivity;
4028
4029 $this->_is_on = true;
4030
4031 return true;
4032 }
4033
4034 /**
4035 * Anonymous and unique site identifier (Hash).
4036 *
4037 * @author Vova Feldman (@svovaf)
4038 * @since 1.1.0
4039 *
4040 * @param null|int $blog_id Since 2.0.0
4041 *
4042 * @return string
4043 */
4044 function get_anonymous_id( $blog_id = null ) {
4045 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4046
4047 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4048 $key = fs_strip_url_protocol( get_site_url( $blog_id ) );
4049
4050 $secure_auth = SECURE_AUTH_KEY;
4051 if ( empty( $secure_auth ) ||
4052 false !== strpos( $secure_auth, ' ' ) ||
4053 'put your unique phrase here' === $secure_auth
4054 ) {
4055 // Protect against default auth key.
4056 $secure_auth = md5( microtime() );
4057 }
4058
4059 /**
4060 * Base the unique identifier on the WP secure authentication key. Which
4061 * turns the key into a secret anonymous identifier. This will help us
4062 * to avoid duplicate installs generation on the backend upon opt-in.
4063 *
4064 * @author Vova Feldman (@svovaf)
4065 * @since 1.2.3
4066 */
4067 $unique_id = md5( $key . $secure_auth );
4068
4069 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4070 }
4071
4072 $this->_logger->departure( $unique_id );
4073
4074 return $unique_id;
4075 }
4076
4077 /**
4078 * @author Vova Feldman (@svovaf)
4079 * @since 1.1.7.4
4080 *
4081 * @return \WP_User
4082 */
4083 static function _get_current_wp_user() {
4084 self::require_pluggable_essentials();
4085 self::wp_cookie_constants();
4086
4087 return wp_get_current_user();
4088 }
4089
4090 /**
4091 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4092 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4093 * is network activated the cookie constants are only configured after the network
4094 * plugins activation, therefore, if we don't define those constants WP will throw
4095 * PHP warnings/notices.
4096 *
4097 * @author Vova Feldman (@svovaf)
4098 * @since 2.1.1
4099 */
4100 private static function wp_cookie_constants() {
4101 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4102 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4103 ) {
4104 return;
4105 }
4106
4107 /**
4108 * Used to guarantee unique hash cookies
4109 *
4110 * @since 1.5.0
4111 */
4112 if ( ! defined( 'COOKIEHASH' ) ) {
4113 $siteurl = get_site_option( 'siteurl' );
4114 if ( $siteurl ) {
4115 define( 'COOKIEHASH', md5( $siteurl ) );
4116 } else {
4117 define( 'COOKIEHASH', '' );
4118 }
4119 }
4120
4121 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4122 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4123 }
4124
4125 /**
4126 * @since 2.5.0
4127 */
4128 if ( ! defined( 'AUTH_COOKIE' ) ) {
4129 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4130 }
4131
4132 /**
4133 * @since 2.6.0
4134 */
4135 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4136 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4137 }
4138 }
4139
4140 /**
4141 * @author Vova Feldman (@svovaf)
4142 * @since 2.1.0
4143 *
4144 * @return int
4145 */
4146 static function get_current_wp_user_id() {
4147 $wp_user = self::_get_current_wp_user();
4148
4149 return $wp_user->ID;
4150 }
4151
4152 /**
4153 * @author Vova Feldman (@svovaf)
4154 * @since 1.2.1.7
4155 *
4156 * @param string $email
4157 *
4158 * @return bool
4159 */
4160 static function is_valid_email( $email ) {
4161 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4162 return false;
4163 }
4164
4165 $parts = explode( '@', $email );
4166
4167 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4168 return false;
4169 }
4170
4171 $blacklist = array(
4172 'admin.',
4173 'webmaster.',
4174 'localhost.',
4175 'dev.',
4176 'development.',
4177 'test.',
4178 'stage.',
4179 'staging.',
4180 );
4181
4182 // Make sure domain is not one of the blacklisted.
4183 foreach ( $blacklist as $invalid ) {
4184 if ( 0 === strpos( $parts[1], $invalid ) ) {
4185 return false;
4186 }
4187 }
4188
4189 // Get the UTF encoded domain name.
4190 $domain = idn_to_ascii( $parts[1] ) . '.';
4191
4192 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4193 }
4194
4195 /**
4196 * Generate API connectivity issue message.
4197 *
4198 * @author Vova Feldman (@svovaf)
4199 * @since 1.0.9
4200 *
4201 * @param mixed $api_result
4202 * @param bool $is_first_failure
4203 */
4204 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
4205 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
4206 // Don't add message if it's the free version and can run anonymously.
4207 return;
4208 }
4209
4210 if ( ! function_exists( 'wp_nonce_url' ) ) {
4211 require_once ABSPATH . 'wp-includes/functions.php';
4212 }
4213
4214 $current_user = self::_get_current_wp_user();
4215 // $admin_email = get_option( 'admin_email' );
4216 $admin_email = $current_user->user_email;
4217
4218 // Aliases.
4219 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
4220 $deactivate_plugin_desc = $this->esc_html_inline( 'We feel your frustration and sincerely apologize for the inconvenience. Hope to see you again in the future.', 'deactivate-plugin-desc' );
4221 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
4222 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
4223 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
4224 $fix_issue_desc = $this->esc_html_inline( 'We will do our best to whitelist your server and resolve this issue ASAP. You will get a follow-up email to %s once we have an update.', 'fix-issue-desc' );
4225 /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
4226 $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
4227 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
4228 $happy_to_resolve_issue_asap = $this->esc_html_inline( 'We are sure it\'s an issue on our side and more than happy to resolve it for you ASAP if you give us a chance.', 'happy-to-resolve-issue-asap' );
4229
4230 $message = false;
4231 if ( is_object( $api_result ) &&
4232 isset( $api_result->error ) &&
4233 isset( $api_result->error->code )
4234 ) {
4235 switch ( $api_result->error->code ) {
4236 case 'curl_missing':
4237 $missing_methods = '';
4238 if ( is_array( $api_result->missing_methods ) &&
4239 ! empty( $api_result->missing_methods )
4240 ) {
4241 foreach ( $api_result->missing_methods as $m ) {
4242 if ( 'curl_version' === $m ) {
4243 continue;
4244 }
4245
4246 if ( ! empty( $missing_methods ) ) {
4247 $missing_methods .= ', ';
4248 }
4249
4250 $missing_methods .= sprintf( '<code>%s</code>', $m );
4251 }
4252
4253 if ( ! empty( $missing_methods ) ) {
4254 $missing_methods = sprintf(
4255 '<br><br><b>%s</b> %s',
4256 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
4257 $missing_methods
4258 );
4259 }
4260 }
4261
4262 $message = sprintf(
4263 $x_requires_access_to_api . ' ' .
4264 $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' ) . ' ' .
4265 $missing_methods .
4266 ' %s',
4267 '<b>' . $this->get_plugin_name() . '</b>',
4268 sprintf(
4269 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4270 sprintf(
4271 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
4272 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
4273 ' - ' . sprintf(
4274 $this->get_text_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'curl-missing-no-clue-desc' ),
4275 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4276 )
4277 ),
4278 sprintf(
4279 '<b>%s</b> - %s',
4280 $sysadmin_title,
4281 esc_html( sprintf( $this->get_text_inline( 'Great, please install cURL and enable it in your php.ini file. In addition, search for the \'disable_functions\' directive in your php.ini file and remove any disabled methods starting with \'curl_\'. To make sure it was successfully activated, use \'phpinfo()\'. Once activated, deactivate the %s and reactivate it back again.', 'curl-missing-sysadmin-desc' ), $this->get_module_label( true ) ) )
4282 ),
4283 sprintf(
4284 '<a href="%s"><b>%s</b></a> - %s',
4285 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4286 $deactivate_plugin_title,
4287 $deactivate_plugin_desc
4288 )
4289 )
4290 );
4291 break;
4292 case 'cloudflare_ddos_protection':
4293 $message = sprintf(
4294 $x_requires_access_to_api . ' ' .
4295 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
4296 $happy_to_resolve_issue_asap .
4297 ' %s',
4298 '<b>' . $this->get_plugin_name() . '</b>',
4299 sprintf(
4300 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4301 sprintf(
4302 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
4303 $fix_issue_title,
4304 ' - ' . sprintf(
4305 $fix_issue_desc,
4306 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4307 )
4308 ),
4309 sprintf(
4310 '<a href="%s" target="_blank" rel="noopener noreferrer"><b>%s</b></a> - %s',
4311 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4312 $install_previous_title,
4313 $install_previous_desc
4314 ),
4315 sprintf(
4316 '<a href="%s"><b>%s</b></a> - %s',
4317 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=' . '', 'deactivate-plugin_' . $this->_plugin_basename ),
4318 $deactivate_plugin_title,
4319 $deactivate_plugin_desc
4320 )
4321 )
4322 );
4323 break;
4324 case 'squid_cache_block':
4325 $message = sprintf(
4326 $x_requires_access_to_api . ' ' .
4327 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
4328 ' %s',
4329 '<b>' . $this->get_plugin_name() . '</b>',
4330 sprintf(
4331 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4332 sprintf(
4333 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
4334 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
4335 sprintf(
4336 $this->esc_html_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'squid-no-clue-desc' ),
4337 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4338 )
4339 ),
4340 sprintf(
4341 '<b>%s</b> - %s',
4342 $sysadmin_title,
4343 sprintf(
4344 $this->esc_html_inline( 'Great, please whitelist the following domains: %s. Once you are done, deactivate the %s and activate it again.', 'squid-sysadmin-desc' ),
4345 // We use a filter since the plugin might require additional API connectivity.
4346 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
4347 'api.freemius.com',
4348 'wp.freemius.com'
4349 ) ) ) . '</b>',
4350 $this->_module_type
4351 )
4352 ),
4353 sprintf(
4354 '<a href="%s"><b>%s</b></a> - %s',
4355 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4356 $deactivate_plugin_title,
4357 $deactivate_plugin_desc
4358 )
4359 )
4360 );
4361 break;
4362 // default:
4363 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
4364 // break;
4365 }
4366 }
4367
4368 $message_id = 'failed_connect_api';
4369 $type = 'error';
4370
4371 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
4372
4373 if ( false === $message ) {
4374 if ( $is_first_failure ) {
4375 // First attempt failed.
4376 $message = sprintf(
4377 $x_requires_access_to_api . ' ' .
4378 $connectivity_test_fails_message . ' ' .
4379 $this->esc_html_inline( 'It\'s probably a temporary issue on our end. Just to be sure, with your permission, would it be o.k to run another connectivity test?', 'connectivity-test-maybe-temporary' ) . '<br><br>' .
4380 '%s',
4381 '<b>' . $this->get_plugin_name() . '</b>',
4382 sprintf(
4383 '<div id="fs_firewall_issue_options">%s %s</div>',
4384 sprintf(
4385 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
4386 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
4387 ),
4388 sprintf(
4389 '<a href="%s" class="button">%s</a>',
4390 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4391 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
4392 )
4393 )
4394 );
4395
4396 $message_id = 'failed_connect_api_first';
4397 $type = 'promotion';
4398 } else {
4399 // Second connectivity attempt failed.
4400 $message = sprintf(
4401 $x_requires_access_to_api . ' ' .
4402 $connectivity_test_fails_message . ' ' .
4403 $happy_to_resolve_issue_asap .
4404 ' %s',
4405 '<b>' . $this->get_plugin_name() . '</b>',
4406 sprintf(
4407 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4408 sprintf(
4409 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
4410 $fix_issue_title,
4411 ' - ' . sprintf(
4412 $fix_issue_desc,
4413 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4414 )
4415 ),
4416 sprintf(
4417 '<a href="%s" target="_blank" rel="noopener noreferrer"><b>%s</b></a> - %s',
4418 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4419 $install_previous_title,
4420 $install_previous_desc
4421 ),
4422 sprintf(
4423 '<a href="%s"><b>%s</b></a> - %s',
4424 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4425 $deactivate_plugin_title,
4426 $deactivate_plugin_desc
4427 )
4428 )
4429 );
4430 }
4431 }
4432
4433 $this->_admin_notices->add_sticky(
4434 $message,
4435 $message_id,
4436 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4437 $type
4438 );
4439 }
4440
4441 /**
4442 * Handle user request to resolve connectivity issue.
4443 * This method will send an email to Freemius API technical staff for resolution.
4444 * The email will contain server's info and installed plugins (might be caching issue).
4445 *
4446 * @author Vova Feldman (@svovaf)
4447 * @since 1.0.9
4448 */
4449 function _email_about_firewall_issue() {
4450 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
4451
4452 $pong = $this->ping();
4453
4454 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4455
4456 if ( $is_connected ) {
4457 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4458
4459 $this->store_connectivity_info( $pong, $is_connected );
4460
4461 echo $this->get_after_plugin_activation_redirect_url();
4462 exit;
4463 }
4464
4465 $current_user = self::_get_current_wp_user();
4466 $admin_email = $current_user->user_email;
4467
4468 $error_type = fs_request_get( 'error_type', 'general' );
4469
4470 switch ( $error_type ) {
4471 case 'squid':
4472 $title = 'Squid ACL Blocking Issue';
4473 break;
4474 case 'cloudflare':
4475 $title = 'CloudFlare Blocking Issue';
4476 break;
4477 default:
4478 $title = 'API Connectivity Issue';
4479 break;
4480 }
4481
4482 $custom_email_sections = array();
4483
4484 // Add 'API Error' custom email section.
4485 $custom_email_sections['api_error'] = array(
4486 'title' => 'API Error',
4487 'rows' => array(
4488 'ping' => array(
4489 'API Error',
4490 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
4491 ),
4492 )
4493 );
4494
4495 // Send email with technical details to resolve API connectivity issues.
4496 $this->send_email(
4497 'api@freemius.com', // recipient
4498 $title . ' [' . $this->get_plugin_name() . ']', // subject
4499 $custom_email_sections,
4500 array( "Reply-To: $admin_email <$admin_email>" ) // headers
4501 );
4502
4503 $this->_admin_notices->add_sticky(
4504 sprintf(
4505 $this->get_text_inline( 'Thank for giving us the chance to fix it! A message was just sent to our technical staff. We will get back to you as soon as we have an update to %s. Appreciate your patience.', 'fix-request-sent-message' ),
4506 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4507 ),
4508 'server_details_sent'
4509 );
4510
4511 // Action was taken, tell that API connectivity troubleshooting should be off now.
4512
4513 echo "1";
4514 exit;
4515 }
4516
4517 /**
4518 * Handle connectivity test retry approved by the user.
4519 *
4520 * @author Vova Feldman (@svovaf)
4521 * @since 1.1.7.4
4522 */
4523 function _retry_connectivity_test() {
4524 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
4525
4526 $pong = $this->ping();
4527
4528 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4529
4530 if ( $is_connected ) {
4531 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4532
4533 $this->store_connectivity_info( $pong, $is_connected );
4534
4535 echo $this->get_after_plugin_activation_redirect_url();
4536 } else {
4537 // Add connectivity issue message after 2nd failed attempt.
4538 $this->_add_connectivity_issue_message( $pong, false );
4539
4540 echo "1";
4541 }
4542
4543 exit;
4544 }
4545
4546 static function _add_firewall_issues_javascript() {
4547 $params = array();
4548 fs_require_once_template( 'firewall-issues-js.php', $params );
4549 }
4550
4551 #endregion
4552
4553 #----------------------------------------------------------------------------------
4554 #region Email
4555 #----------------------------------------------------------------------------------
4556
4557 /**
4558 * Generates and sends an HTML email with customizable sections.
4559 *
4560 * @author Leo Fajardo (@leorw)
4561 * @since 1.1.2
4562 *
4563 * @param string $to_address
4564 * @param string $subject
4565 * @param array $sections
4566 * @param array $headers
4567 *
4568 * @return bool Whether the email contents were sent successfully.
4569 */
4570 private function send_email(
4571 $to_address,
4572 $subject,
4573 $sections = array(),
4574 $headers = array()
4575 ) {
4576 $default_sections = $this->get_email_sections();
4577
4578 // Insert new sections or replace the default email sections.
4579 if ( is_array( $sections ) && ! empty( $sections ) ) {
4580 foreach ( $sections as $section_id => $custom_section ) {
4581 if ( ! isset( $default_sections[ $section_id ] ) ) {
4582 // If the section does not exist, add it.
4583 $default_sections[ $section_id ] = $custom_section;
4584 } else {
4585 // If the section already exists, override it.
4586 $current_section = $default_sections[ $section_id ];
4587
4588 // Replace the current section's title if a custom section title exists.
4589 if ( isset( $custom_section['title'] ) ) {
4590 $current_section['title'] = $custom_section['title'];
4591 }
4592
4593 // Insert new rows under the current section or replace the default rows.
4594 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4595 foreach ( $custom_section['rows'] as $row_id => $row ) {
4596 $current_section['rows'][ $row_id ] = $row;
4597 }
4598 }
4599
4600 $default_sections[ $section_id ] = $current_section;
4601 }
4602 }
4603 }
4604
4605 $vars = array( 'sections' => $default_sections );
4606 $message = fs_get_template( 'email.php', $vars );
4607
4608 // Set the type of email to HTML.
4609 $headers[] = 'Content-type: text/html; charset=UTF-8';
4610
4611 $header_string = implode( "\r\n", $headers );
4612
4613 return wp_mail(
4614 $to_address,
4615 $subject,
4616 $message,
4617 $header_string
4618 );
4619 }
4620
4621 /**
4622 * Generates the data for the sections of the email content.
4623 *
4624 * @author Leo Fajardo (@leorw)
4625 * @since 1.1.2
4626 *
4627 * @return array
4628 */
4629 private function get_email_sections() {
4630 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4631 $current_user = self::_get_current_wp_user();
4632
4633 // Retrieve the cURL version information so that we can get the version number below.
4634 $curl_version_information = curl_version();
4635
4636 $active_plugin = self::get_active_plugins();
4637
4638 // Generate the list of active plugins separated by new line.
4639 $active_plugin_string = '';
4640 foreach ( $active_plugin as $plugin ) {
4641 $active_plugin_string .= sprintf(
4642 '<a href="%s">%s</a> [v%s]<br>',
4643 $plugin['PluginURI'],
4644 $plugin['Name'],
4645 $plugin['Version']
4646 );
4647 }
4648
4649 $server_ip = WP_FS__REMOTE_ADDR;
4650
4651 // Add PHP info for deeper investigation.
4652 ob_start();
4653 phpinfo();
4654 $php_info = ob_get_clean();
4655
4656 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4657
4658 // Generate the default email sections.
4659 $sections = array(
4660 'sdk' => array(
4661 'title' => 'SDK',
4662 'rows' => array(
4663 'fs_version' => array( 'FS Version', $this->version ),
4664 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4665 )
4666 ),
4667 'plugin' => array(
4668 'title' => ucfirst( $this->get_module_type() ),
4669 'rows' => array(
4670 'name' => array( 'Name', $this->get_plugin_name() ),
4671 'version' => array( 'Version', $this->get_plugin_version() )
4672 )
4673 ),
4674 'api' => array(
4675 'title' => 'API Subdomain',
4676 'rows' => array(
4677 'dns' => array(
4678 'DNS_CNAME',
4679 function_exists( 'dns_get_record' ) ?
4680 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4681 'dns_get_record() disabled/blocked'
4682 ),
4683 'ip' => array(
4684 'IP',
4685 function_exists( 'gethostbyname' ) ?
4686 gethostbyname( $api_domain ) :
4687 'gethostbyname() disabled/blocked'
4688 ),
4689 ),
4690 ),
4691 'site' => array(
4692 'title' => 'Site',
4693 'rows' => array(
4694 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4695 'address' => array( 'Address', site_url() ),
4696 'host' => array(
4697 'HTTP_HOST',
4698 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4699 ),
4700 'hosting' => array(
4701 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4702 fs_request_get( 'hosting_company' ) :
4703 'Unknown',
4704 ),
4705 'server_addr' => array(
4706 'SERVER_ADDR',
4707 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4708 )
4709 )
4710 ),
4711 'user' => array(
4712 'title' => 'User',
4713 'rows' => array(
4714 'email' => array( 'Email', $current_user->user_email ),
4715 'first' => array( 'First', $current_user->user_firstname ),
4716 'last' => array( 'Last', $current_user->user_lastname )
4717 )
4718 ),
4719 'plugins' => array(
4720 'title' => 'Plugins',
4721 'rows' => array(
4722 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4723 )
4724 ),
4725 'php_info' => array(
4726 'title' => 'PHP Info',
4727 'rows' => array(
4728 'info' => array( $php_info )
4729 ),
4730 )
4731 );
4732
4733 // Allow the sections to be modified by other code.
4734 $sections = $this->apply_filters( 'email_template_sections', $sections );
4735
4736 return $sections;
4737 }
4738
4739 #endregion
4740
4741 #----------------------------------------------------------------------------------
4742 #region Initialization
4743 #----------------------------------------------------------------------------------
4744
4745 /**
4746 * Init plugin's Freemius instance.
4747 *
4748 * @author Vova Feldman (@svovaf)
4749 * @since 1.0.1
4750 *
4751 * @param number $id
4752 * @param string $public_key
4753 * @param bool $is_live
4754 * @param bool $is_premium
4755 */
4756 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4757 $this->_logger->entrance();
4758
4759 $this->dynamic_init( array(
4760 'id' => $id,
4761 'public_key' => $public_key,
4762 'is_live' => $is_live,
4763 'is_premium' => $is_premium,
4764 ) );
4765 }
4766
4767 /**
4768 * Dynamic initiator, originally created to support initiation
4769 * with parent_id for add-ons.
4770 *
4771 * @author Vova Feldman (@svovaf)
4772 * @since 1.0.6
4773 *
4774 * @param array $plugin_info
4775 *
4776 * @throws Freemius_Exception
4777 */
4778 function dynamic_init( array $plugin_info ) {
4779 $this->_logger->entrance();
4780
4781 $this->parse_settings( $plugin_info );
4782
4783 $this->register_after_settings_parse_hooks();
4784
4785 if ( $this->should_stop_execution() ) {
4786 return;
4787 }
4788
4789 if ( ! $this->is_registered() ) {
4790 if ( $this->is_anonymous() ) {
4791 // If user skipped, no need to test connectivity.
4792 $this->_has_api_connection = true;
4793 $this->_is_on = true;
4794 } else {
4795 if ( ! $this->has_api_connectivity() ) {
4796 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
4797 $this->_admin_notices->has_sticky( 'failed_connect_api' )
4798 ) {
4799 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
4800 // If anonymous mode is disabled, add firewall admin-notice message.
4801 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
4802
4803 $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' );
4804 add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array(
4805 &$this,
4806 '_email_about_firewall_issue'
4807 ) );
4808
4809 add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array(
4810 &$this,
4811 '_retry_connectivity_test'
4812 ) );
4813
4814 /**
4815 * Currently the admin notice manager relies on the module's type and slug. The new AJAX actions manager uses module IDs, hence, consider to replace the if block above with the commented code below after adjusting the admin notices manager to work with module IDs.
4816 *
4817 * @author Vova Feldman (@svovaf)
4818 * @since 2.0.0
4819 */
4820 /*$this->add_ajax_action( 'resolve_firewall_issues', array(
4821 &$this,
4822 '_email_about_firewall_issue'
4823 ) );
4824
4825 $this->add_ajax_action( 'retry_connectivity_test', array(
4826 &$this,
4827 '_retry_connectivity_test'
4828 ) );*/
4829 }
4830 }
4831
4832 return;
4833 } else {
4834 $this->_admin_notices->remove_sticky( array(
4835 'failed_connect_api_first',
4836 'failed_connect_api',
4837 ) );
4838
4839 if ( $this->_anonymous_mode ) {
4840 // Simulate anonymous mode.
4841 $this->_is_anonymous = true;
4842 }
4843 }
4844 }
4845 }
4846
4847 /**
4848 * This should be executed even if Freemius is off for the core module,
4849 * otherwise, the add-ons dialogbox won't work properly. This is esepcially
4850 * relevant when the developer decided to turn FS off for existing users.
4851 *
4852 * @author Vova Feldman (@svovaf)
4853 */
4854 if ( $this->is_user_in_admin() &&
4855 'plugin-information' === fs_request_get( 'tab', false ) &&
4856 $this->should_use_freemius_updater_and_dialog() &&
4857 (
4858 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4859 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4860 )
4861 ) {
4862 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4863
4864 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4865 }
4866
4867 // Check if Freemius is on for the current plugin.
4868 // This MUST be executed after all the plugin variables has been loaded.
4869 if ( ! $this->is_registered() && ! $this->is_on() ) {
4870 return;
4871 }
4872
4873 if ( $this->has_api_connectivity() ) {
4874 if ( self::is_cron() ) {
4875 $this->hook_callback_to_sync_cron();
4876 } else if ( $this->is_user_in_admin() ) {
4877 /**
4878 * Schedule daily data sync cron if:
4879 *
4880 * 1. User opted-in (for tracking).
4881 * 2. If skipped, but later upgraded (opted-in via upgrade).
4882 *
4883 * @author Vova Feldman (@svovaf)
4884 * @since 1.1.7.3
4885 *
4886 */
4887 if ( $this->is_registered() ) {
4888 if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) {
4889 $this->schedule_sync_cron();
4890 }
4891 }
4892
4893 /**
4894 * Check if requested for manual blocking background sync.
4895 */
4896 if ( fs_request_has( 'background_sync' ) ) {
4897 $this->run_manual_sync();
4898 }
4899 }
4900 }
4901
4902 if ( $this->is_registered() ) {
4903 $this->hook_callback_to_install_sync();
4904 }
4905
4906 if ( $this->is_addon() ) {
4907 if ( $this->is_parent_plugin_installed() ) {
4908 // Link to parent FS.
4909 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4910
4911 // Get parent plugin reference.
4912 $this->_parent_plugin = $this->_parent->get_plugin();
4913 }
4914 }
4915
4916 if ( $this->is_user_in_admin() ) {
4917 if ( $this->is_addon() ) {
4918 if ( ! $this->is_parent_plugin_installed() ) {
4919 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4920
4921 if ( isset( $plugin_info['parent'] ) ) {
4922 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4923 }
4924
4925 $this->_admin_notices->add(
4926 ( ! empty( $parent_name ) ?
4927 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 ) :
4928 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() )
4929 ),
4930 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4931 'error'
4932 );
4933
4934 return;
4935 } else {
4936 $is_network_admin = fs_is_network_admin();
4937
4938 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4939 // If add-on activated and parent not, automatically install parent for the user.
4940 $this->activate_parent_account( $this->_parent );
4941 } else if (
4942 $this->_parent->is_registered() &&
4943 ! $this->is_registered() &&
4944 /**
4945 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4946 * * Network active and in network admin - network activate add-on account.
4947 * * Network active and not in network admin - activate add-on account for the current blog.
4948 * * Not network active and not in network admin - activate add-on account for the current blog.
4949 *
4950 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4951 *
4952 * @author Leo Fajardo (@leorw)
4953 * @since 2.3.0
4954 */
4955 ( $this->is_network_active() || ! $is_network_admin )
4956 ) {
4957 $premium_license = null;
4958
4959 if (
4960 ! $this->has_free_plan() &&
4961 $this->is_bundle_license_auto_activation_enabled() &&
4962 $this->_parent->is_activated_with_bundle_license()
4963 ) {
4964 /**
4965 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4966 *
4967 * @author Leo Fajardo (@leorw)
4968 * @since 2.4.0
4969 */
4970 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4971
4972 if (
4973 is_object( $bundle_license ) &&
4974 ! empty( $bundle_license->products ) &&
4975 in_array( $this->get_id(), $bundle_license->products )
4976 ) {
4977 $premium_license = $bundle_license;
4978 }
4979 }
4980
4981 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4982 // If parent plugin activated, automatically install add-on for the user.
4983 $this->_activate_addon_account(
4984 $this->_parent,
4985 ( $this->is_network_active() && $is_network_admin ) ?
4986 true :
4987 get_current_blog_id(),
4988 $premium_license
4989 );
4990 }
4991 }
4992
4993 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4994 if ( $this->is_premium() ) {
4995 // Remove add-on download admin-notice.
4996 $this->_parent->_admin_notices->remove_sticky( array(
4997 'addon_plan_upgraded_' . $this->_slug,
4998 'no_addon_license_' . $this->_slug,
4999 ) );
5000 }
5001
5002 // $this->deactivate_premium_only_addon_without_license();
5003 }
5004 }
5005
5006 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
5007
5008 // if ( $this->is_registered() ||
5009 // $this->is_anonymous() ||
5010 // $this->is_pending_activation()
5011 // ) {
5012 // $this->_init_admin();
5013 // }
5014 }
5015
5016 /**
5017 * Should be called outside `$this->is_user_in_admin()` scope
5018 * because the updater has some logic that needs to be executed
5019 * during AJAX calls.
5020 *
5021 * Currently we need to hook to the `http_request_host_is_external` filter.
5022 * In the future, there might be additional logic added.
5023 *
5024 * @author Vova Feldman
5025 * @since 1.2.1.6
5026 */
5027 if (
5028 $this->should_use_freemius_updater_and_dialog() &&
5029 (
5030 $this->is_premium() ||
5031 /**
5032 * If not premium but the premium version is installed, also instantiate the updater so that the
5033 * plugin information dialog of the premium version will have the information from the server.
5034 *
5035 * @author Leo Fajardo (@leorw)
5036 * @since 2.2.3
5037 */
5038 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
5039 ) &&
5040 $this->has_release_on_freemius()
5041 ) {
5042 FS_Plugin_Updater::instance( $this );
5043 }
5044
5045 $this->do_action( 'initiated' );
5046
5047 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
5048 if ( isset( $this->_storage->prev_is_premium ) ) {
5049 $this->apply_filters(
5050 'after_code_type_change',
5051 // New code type.
5052 $this->_plugin->is_premium
5053 );
5054 } else {
5055 // Set for code type for the first time.
5056 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5057 }
5058 }
5059
5060 if ( ! $this->is_addon() ) {
5061 if ( $this->is_registered() ) {
5062 // Fix for upgrade from versions < 1.0.9.
5063 if ( ! isset( $this->_storage->activation_timestamp ) ) {
5064 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
5065 }
5066
5067 $this->do_action( 'after_init_plugin_registered' );
5068 } else if ( $this->is_anonymous() ) {
5069 $this->do_action( 'after_init_plugin_anonymous' );
5070 } else if ( $this->is_pending_activation() ) {
5071 $this->do_action( 'after_init_plugin_pending_activations' );
5072 }
5073 } else {
5074 if ( $this->is_registered() ) {
5075 $this->do_action( 'after_init_addon_registered' );
5076 } else if ( $this->is_anonymous() ) {
5077 $this->do_action( 'after_init_addon_anonymous' );
5078 } else if ( $this->is_pending_activation() ) {
5079 $this->do_action( 'after_init_addon_pending_activations' );
5080 }
5081 }
5082 }
5083
5084 /**
5085 * @author Leo Fajardo (@leorw)
5086 * @since 2.2.3
5087 *
5088 * @return bool
5089 */
5090 private function should_use_freemius_updater_and_dialog() {
5091 return (
5092 /**
5093 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5094 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5095 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5096 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5097 * plugin details from .org).
5098 */
5099 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5100 (
5101 ! self::is_plugin_install_page() &&
5102 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5103 ( 'install-plugin' !== fs_request_get( 'action' ) )
5104 )
5105 );
5106 }
5107
5108 /**
5109 * @author Leo Fajardo (@leorw)
5110 *
5111 * @since 1.2.1.5
5112 */
5113 function _stop_tracking_callback() {
5114 $this->_logger->entrance();
5115
5116 $this->check_ajax_referer( 'stop_tracking' );
5117
5118 $result = $this->stop_tracking( fs_is_network_admin() );
5119
5120 if ( true === $result ) {
5121 self::shoot_ajax_success();
5122 }
5123
5124 $this->_logger->api_error( $result );
5125
5126 self::shoot_ajax_failure(
5127 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
5128 ( $this->is_api_error( $result ) && isset( $result->error ) ?
5129 $result->error->message :
5130 var_export( $result, true ) )
5131 );
5132 }
5133
5134 /**
5135 * @author Leo Fajardo (@leorw)
5136 * @since 1.2.1.5
5137 */
5138 function _allow_tracking_callback() {
5139 $this->_logger->entrance();
5140
5141 $this->check_ajax_referer( 'allow_tracking' );
5142
5143 $result = $this->allow_tracking( fs_is_network_admin() );
5144
5145 if ( true === $result ) {
5146 self::shoot_ajax_success();
5147 }
5148
5149 $this->_logger->api_error( $result );
5150
5151 self::shoot_ajax_failure(
5152 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
5153 ( $this->is_api_error( $result ) && isset( $result->error ) ?
5154 $result->error->message :
5155 var_export( $result, true ) )
5156 );
5157 }
5158
5159 /**
5160 * Opt-out from usage tracking.
5161 *
5162 * Note: This will not delete the account information but will stop all tracking.
5163 *
5164 * Returns:
5165 * 1. FALSE - If the user never opted-in.
5166 * 2. TRUE - If successfully opted-out.
5167 * 3. object - API result on failure.
5168 *
5169 * @author Leo Fajardo (@leorw)
5170 * @since 1.2.1.5
5171 *
5172 * @return bool|object
5173 */
5174 function stop_site_tracking() {
5175 $this->_logger->entrance();
5176
5177 if ( ! $this->is_registered() ) {
5178 // User never opted-in.
5179 return false;
5180 }
5181
5182 if ( $this->is_tracking_prohibited() ) {
5183 // Already disconnected.
5184 return true;
5185 }
5186
5187 // Send update to FS.
5188 $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array(
5189 'is_disconnected' => true
5190 ) );
5191
5192 if ( ! $this->is_api_result_entity( $result ) ||
5193 ! isset( $result->is_disconnected ) ||
5194 ! $result->is_disconnected
5195 ) {
5196 $this->_logger->api_error( $result );
5197
5198 return $result;
5199 }
5200
5201 $this->_site->is_disconnected = $result->is_disconnected;
5202 $this->_store_site();
5203
5204 $this->clear_sync_cron();
5205
5206 // Successfully disconnected.
5207 return true;
5208 }
5209
5210 /**
5211 * Opt-out network from usage tracking.
5212 *
5213 * Note: This will not delete the account information but will stop all tracking.
5214 *
5215 * Returns:
5216 * 1. FALSE - If the user never opted-in.
5217 * 2. TRUE - If successfully opted-out.
5218 * 3. object - API result on failure.
5219 *
5220 * @author Leo Fajardo (@leorw)
5221 * @since 1.2.1.5
5222 *
5223 * @return bool|object
5224 */
5225 function stop_network_tracking() {
5226 $this->_logger->entrance();
5227
5228 if ( ! $this->is_registered() ) {
5229 // User never opted-in.
5230 return false;
5231 }
5232
5233 $install_id_2_blog_id = array();
5234 $installs_map = $this->get_blog_install_map();
5235
5236 $opt_out_all = true;
5237
5238 $params = array();
5239 foreach ( $installs_map as $blog_id => $install ) {
5240 if ( $install->is_tracking_prohibited() ) {
5241 // Already opted-out.
5242 continue;
5243 }
5244
5245 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5246 // Opt-out only from non-delegated installs.
5247 $opt_out_all = false;
5248 continue;
5249 }
5250
5251 $params[] = array( 'id' => $install->id );
5252
5253 $install_id_2_blog_id[ $install->id ] = $blog_id;
5254 }
5255
5256 if ( empty( $install_id_2_blog_id ) ) {
5257 return true;
5258 }
5259
5260 $params[] = array( 'is_disconnected' => true );
5261
5262 // Send update to FS.
5263 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5264
5265 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5266 $this->_logger->api_error( $result );
5267
5268 return $result;
5269 }
5270
5271 foreach ( $result->installs as $r_install ) {
5272 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5273 $install = $installs_map[ $blog_id ];
5274 $install->is_disconnected = $r_install->is_disconnected;
5275 $this->_store_site( true, $blog_id, $install );
5276 }
5277
5278 $this->clear_sync_cron( $opt_out_all );
5279
5280 // Successfully disconnected.
5281 return true;
5282 }
5283
5284 /**
5285 * Opt-out from usage tracking.
5286 *
5287 * Note: This will not delete the account information but will stop all tracking.
5288 *
5289 * Returns:
5290 * 1. FALSE - If the user never opted-in.
5291 * 2. TRUE - If successfully opted-out.
5292 * 3. object - API result on failure.
5293 *
5294 * @author Leo Fajardo (@leorw)
5295 * @since 1.2.1.5
5296 *
5297 * @param bool $is_network_action
5298 *
5299 * @return bool|object
5300 */
5301 function stop_tracking( $is_network_action = false ) {
5302 $this->_logger->entrance();
5303
5304 return $is_network_action ?
5305 $this->stop_network_tracking() :
5306 $this->stop_site_tracking();
5307 }
5308
5309 /**
5310 * Opt-in back into usage tracking.
5311 *
5312 * Note: This will only work if the user opted-in previously.
5313 *
5314 * Returns:
5315 * 1. FALSE - If the user never opted-in.
5316 * 2. TRUE - If successfully opted-in back to usage tracking.
5317 * 3. object - API result on failure.
5318 *
5319 * @author Leo Fajardo (@leorw)
5320 * @since 1.2.1.5
5321 *
5322 * @return bool|object
5323 */
5324 function allow_site_tracking() {
5325 $this->_logger->entrance();
5326
5327 if ( ! $this->is_registered() ) {
5328 // User never opted-in.
5329 return false;
5330 }
5331
5332 if ( $this->is_tracking_allowed() ) {
5333 // Tracking already allowed.
5334 return true;
5335 }
5336
5337 $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array(
5338 'is_disconnected' => false
5339 ) );
5340
5341 if ( ! $this->is_api_result_entity( $result ) ||
5342 ! isset( $result->is_disconnected ) ||
5343 $result->is_disconnected
5344 ) {
5345 $this->_logger->api_error( $result );
5346
5347 return $result;
5348 }
5349
5350 $this->_site->is_disconnected = $result->is_disconnected;
5351 $this->_store_site();
5352
5353 $this->schedule_sync_cron();
5354
5355 // Successfully reconnected.
5356 return true;
5357 }
5358
5359 /**
5360 * Opt-in network back into usage tracking.
5361 *
5362 * Note: This will only work if the user opted-in previously.
5363 *
5364 * Returns:
5365 * 1. FALSE - If the user never opted-in.
5366 * 2. TRUE - If successfully opted-in back to usage tracking.
5367 * 3. object - API result on failure.
5368 *
5369 * @author Leo Fajardo (@leorw)
5370 * @since 1.2.1.5
5371 *
5372 * @return bool|object
5373 */
5374 function allow_network_tracking() {
5375 $this->_logger->entrance();
5376
5377 if ( ! $this->is_registered() ) {
5378 // User never opted-in.
5379 return false;
5380 }
5381
5382 $install_id_2_blog_id = array();
5383 $installs_map = $this->get_blog_install_map();
5384
5385 $params = array();
5386 foreach ( $installs_map as $blog_id => $install ) {
5387 if ( $install->is_tracking_allowed() ) {
5388 continue;
5389 }
5390
5391 $params[] = array( 'id' => $install->id );
5392
5393 $install_id_2_blog_id[ $install->id ] = $blog_id;
5394 }
5395
5396 if ( empty( $install_id_2_blog_id ) ) {
5397 return true;
5398 }
5399
5400 $params[] = array( 'is_disconnected' => false );
5401
5402 // Send update to FS.
5403 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5404
5405
5406 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5407 $this->_logger->api_error( $result );
5408
5409 return $result;
5410 }
5411
5412 foreach ( $result->installs as $r_install ) {
5413 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5414 $install = $installs_map[ $blog_id ];
5415 $install->is_disconnected = $r_install->is_disconnected;
5416 $this->_store_site( true, $blog_id, $install );
5417 }
5418
5419 $this->schedule_sync_cron();
5420
5421 // Successfully reconnected.
5422 return true;
5423 }
5424
5425 /**
5426 * Opt-in back into usage tracking.
5427 *
5428 * Note: This will only work if the user opted-in previously.
5429 *
5430 * Returns:
5431 * 1. FALSE - If the user never opted-in.
5432 * 2. TRUE - If successfully opted-in back to usage tracking.
5433 * 3. object - API result on failure.
5434 *
5435 * @author Leo Fajardo (@leorw)
5436 * @since 1.2.1.5
5437 *
5438 * @param bool $is_network_action
5439 *
5440 * @return bool|object
5441 */
5442 function allow_tracking( $is_network_action = false ) {
5443 $this->_logger->entrance();
5444
5445 return $is_network_action ?
5446 $this->allow_network_tracking() :
5447 $this->allow_site_tracking();
5448 }
5449
5450 /**
5451 * If user opted-in and later disabled usage-tracking,
5452 * re-allow tracking for licensing and updates.
5453 *
5454 * @author Leo Fajardo (@leorw)
5455 * @since 1.2.1.5
5456 *
5457 * @param bool $is_context_single_site
5458 */
5459 private function reconnect_locally( $is_context_single_site = false ) {
5460 $this->_logger->entrance();
5461
5462 if ( ! $this->is_registered() ) {
5463 return;
5464 }
5465
5466 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5467 if ( $this->is_tracking_prohibited() ) {
5468 $this->_site->is_disconnected = false;
5469 $this->_store_site();
5470 }
5471 } else {
5472 $installs_map = $this->get_blog_install_map();
5473 foreach ( $installs_map as $blog_id => $install ) {
5474 /**
5475 * @var FS_Site $install
5476 */
5477 if ( $install->is_tracking_prohibited() ) {
5478 $install->is_disconnected = false;
5479 $this->_store_site( true, $blog_id, $install );
5480 }
5481 }
5482 }
5483 }
5484
5485 /**
5486 * @author Vova Feldman (@svovaf)
5487 * @since 2.3.2
5488 *
5489 * @return bool
5490 */
5491 function is_extensions_tracking_allowed() {
5492 return ( true === $this->apply_filters(
5493 'is_extensions_tracking_allowed',
5494 $this->_storage->get( 'is_extensions_tracking_allowed', null )
5495 ) );
5496 }
5497
5498 /**
5499 * @author Vova Feldman (@svovaf)
5500 * @since 2.3.2
5501 */
5502 function _update_tracking_permission_callback() {
5503 $this->_logger->entrance();
5504
5505 $this->check_ajax_referer( 'update_tracking_permission' );
5506
5507 $is_enabled = fs_request_get_bool( 'is_enabled', null );
5508
5509 if ( ! is_bool( $is_enabled ) ) {
5510 self::shoot_ajax_failure();
5511 }
5512
5513 $permission = fs_request_get( 'permission' );
5514
5515 switch ( $permission ) {
5516 case 'extensions':
5517 $this->update_extensions_tracking_flag( $is_enabled );
5518 break;
5519 default:
5520 $permission = 'no_match';
5521 }
5522
5523 if ( 'no_match' === $permission ) {
5524 self::shoot_ajax_failure();
5525 }
5526
5527 self::shoot_ajax_success( array(
5528 'permissions' => array(
5529 $permission => $is_enabled,
5530 )
5531 ) );
5532 }
5533
5534 /**
5535 * @author Leo Fajardo (@leorw)
5536 * @since 2.3.2
5537 *
5538 * @param bool|null $is_enabled
5539 */
5540 function update_extensions_tracking_flag( $is_enabled ) {
5541 if ( is_bool( $is_enabled ) ) {
5542 $this->_storage->store( 'is_extensions_tracking_allowed', $is_enabled );
5543 }
5544 }
5545
5546 /**
5547 * Parse plugin's settings (as defined by the plugin dev).
5548 *
5549 * @author Vova Feldman (@svovaf)
5550 * @since 1.1.7.3
5551 *
5552 * @param array $plugin_info
5553 *
5554 * @throws \Freemius_Exception
5555 */
5556 private function parse_settings( &$plugin_info ) {
5557 $this->_logger->entrance();
5558
5559 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5560 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5561 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5562 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5563 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5564
5565 /**
5566 * @author Vova Feldman (@svovaf)
5567 * @since 1.1.9 Try to pull secret key from external config.
5568 */
5569 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5570 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5571 }
5572
5573 if ( isset( $plugin_info['parent'] ) ) {
5574 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5575 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5576 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5577 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5578 }
5579
5580 if ( false === $id ) {
5581 throw new Freemius_Exception( array(
5582 'error' => array(
5583 'type' => 'ParameterNotSet',
5584 'message' => 'Plugin id parameter is not set.',
5585 'code' => 'plugin_id_not_set',
5586 'http' => 500,
5587 )
5588 ) );
5589 }
5590 if ( false === $public_key ) {
5591 throw new Freemius_Exception( array(
5592 'error' => array(
5593 'type' => 'ParameterNotSet',
5594 'message' => 'Plugin public_key parameter is not set.',
5595 'code' => 'plugin_public_key_not_set',
5596 'http' => 500,
5597 )
5598 ) );
5599 }
5600
5601 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5602 $this->_plugin :
5603 new FS_Plugin();
5604
5605 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5606
5607 $plugin->update( array(
5608 'id' => $id,
5609 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5610 'public_key' => $public_key,
5611 'slug' => $this->_slug,
5612 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5613 'parent_plugin_id' => $parent_id,
5614 'version' => $this->get_plugin_version(),
5615 'title' => $this->get_plugin_name( $premium_suffix ),
5616 'file' => $this->_plugin_basename,
5617 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5618 'premium_suffix' => $premium_suffix,
5619 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5620 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5621 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5622 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5623 ) );
5624
5625 if ( $plugin->is_updated() ) {
5626 // Update plugin details.
5627 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5628 }
5629 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5630 $this->_plugin->secret_key = $secret_key;
5631
5632 /**
5633 * 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).
5634 *
5635 * @author Vova Feldman
5636 * @since 2.4.5
5637 */
5638 if ( $this->is_network_active() && fs_is_network_admin() ) {
5639 if ( isset( $plugin_info['menu_network'] ) &&
5640 is_array( $plugin_info['menu_network'] ) &&
5641 ! empty( $plugin_info['menu_network'] )
5642 ) {
5643 $plugin_info['menu'] = $plugin_info['menu_network'];
5644 }
5645 }
5646
5647 if ( ! isset( $plugin_info['menu'] ) ) {
5648 $plugin_info['menu'] = array();
5649
5650 if ( ! empty( $this->_storage->sdk_last_version ) &&
5651 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5652 ) {
5653 // Backward compatibility to 1.1.2
5654 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5655 $plugin_info['menu_slug'] :
5656 $this->_slug;
5657 }
5658 }
5659
5660 $this->_menu = FS_Admin_Menu_Manager::instance(
5661 $this->_module_id,
5662 $this->_module_type,
5663 $this->get_unique_affix()
5664 );
5665
5666 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5667
5668 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5669 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5670 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5671 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5672 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5673 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5674 if ( $this->_is_premium_only ) {
5675 // If premium only plugin, disable anonymous mode.
5676 $this->_enable_anonymous = false;
5677 $this->_anonymous_mode = false;
5678 } else {
5679 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5680 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5681 }
5682 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5683 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5684
5685 if ( ! empty( $plugin_info['trial'] ) ) {
5686 $this->_trial_days = $this->get_numeric_option(
5687 $plugin_info['trial'],
5688 'days',
5689 // Default to 0 - trial without days specification.
5690 0
5691 );
5692
5693 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5694 }
5695
5696 $this->_navigation = $this->get_option(
5697 $plugin_info,
5698 'navigation',
5699 $this->is_free_wp_org_theme() ?
5700 self::NAVIGATION_TABS :
5701 self::NAVIGATION_MENU
5702 );
5703 }
5704
5705 /**
5706 * @param string[] $options
5707 * @param string $key
5708 * @param mixed $default
5709 *
5710 * @return bool
5711 */
5712 private function get_option( &$options, $key, $default = false ) {
5713 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5714 }
5715
5716 private function get_bool_option( &$options, $key, $default = false ) {
5717 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5718 }
5719
5720 private function get_numeric_option( &$options, $key, $default = false ) {
5721 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5722 }
5723
5724 /**
5725 * Gate keeper.
5726 *
5727 * @author Vova Feldman (@svovaf)
5728 * @since 1.1.7.3
5729 *
5730 * @return bool
5731 */
5732 private function should_stop_execution() {
5733 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5734 /**
5735 * Don't execute Freemius until plugin was fully loaded at least once,
5736 * to give the opportunity for the activation hook to run before pinging
5737 * the API for connectivity test. This logic is relevant for the
5738 * identification of new plugin install vs. plugin update.
5739 *
5740 * @author Vova Feldman (@svovaf)
5741 * @since 1.1.9
5742 */
5743 return true;
5744 }
5745
5746 if ( $this->is_activation_mode() ) {
5747 if ( ! is_admin() ) {
5748 /**
5749 * If in activation mode, don't execute Freemius outside of the
5750 * admin dashboard.
5751 *
5752 * @author Vova Feldman (@svovaf)
5753 * @since 1.1.7.3
5754 */
5755 return true;
5756 }
5757
5758 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5759 /**
5760 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5761 * then don't start Freemius.
5762 *
5763 * @author Vova Feldman (@svovaf)
5764 * @since 1.1.6.3
5765 *
5766 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5767 */
5768 return true;
5769 }
5770
5771 if ( self::is_cron() ) {
5772 /**
5773 * If in activation mode, don't execute Freemius during wp crons
5774 * (wp crons have HTTP context - called as HTTP request).
5775 *
5776 * @author Vova Feldman (@svovaf)
5777 * @since 1.1.7.3
5778 */
5779 return true;
5780 }
5781
5782 if ( self::is_ajax() &&
5783 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
5784 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
5785 ) {
5786 /**
5787 * During activation, if running in AJAX mode, unless there's a sticky
5788 * connectivity issue notice, don't run Freemius.
5789 *
5790 * @author Vova Feldman (@svovaf)
5791 * @since 1.1.7.3
5792 */
5793 return true;
5794 }
5795 }
5796
5797 return false;
5798 }
5799
5800 /**
5801 * Triggered after code type has changed.
5802 *
5803 * @author Vova Feldman (@svovaf)
5804 * @since 1.1.9.1
5805 */
5806 function _after_code_type_change() {
5807 $this->_logger->entrance();
5808
5809 if ( $this->is_theme() ) {
5810 // Expire the cache of the previous tabs since the theme may
5811 // have setting updates after code type has changed.
5812 $this->_cache->expire( 'tabs' );
5813 $this->_cache->expire( 'tabs_stylesheets' );
5814 }
5815
5816 if ( $this->is_registered() ) {
5817 if ( ! $this->is_addon() ) {
5818 add_action(
5819 is_admin() ? 'admin_init' : 'init',
5820 array( &$this, '_plugin_code_type_changed' )
5821 );
5822 }
5823
5824 if ( $this->is_premium() ) {
5825 // Purge cached payments after switching to the premium version.
5826 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5827 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5828 }
5829 }
5830 }
5831
5832 /**
5833 * Handles plugin's code type change (free <--> premium).
5834 *
5835 * @author Vova Feldman (@svovaf)
5836 * @since 1.0.9
5837 */
5838 function _plugin_code_type_changed() {
5839 $this->_logger->entrance();
5840
5841 if ( $this->is_premium() ) {
5842 $this->reconnect_locally();
5843
5844 // Activated premium code.
5845 $this->do_action( 'after_premium_version_activation' );
5846
5847 // Remove all sticky messages related to download of the premium version.
5848 $this->_admin_notices->remove_sticky( array(
5849 'trial_started',
5850 'plan_upgraded',
5851 'plan_changed',
5852 'license_activated',
5853 ) );
5854
5855 $notice = '';
5856 if ( ! $this->is_only_premium() ) {
5857 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5858 }
5859
5860 $license_notice = $this->get_license_network_activation_notice();
5861 if ( ! empty( $license_notice ) ) {
5862 $notice .= ' ' . $license_notice;
5863 }
5864
5865 if ( ! empty( $notice ) ) {
5866 $this->_admin_notices->add_sticky(
5867 trim( $notice ),
5868 'premium_activated',
5869 $this->get_text_x_inline( 'W00t',
5870 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5871 );
5872 }
5873 } else {
5874 // Remove sticky message related to premium code activation.
5875 $this->_admin_notices->remove_sticky( 'premium_activated' );
5876
5877 // Activated free code (after had the premium before).
5878 $this->do_action( 'after_free_version_reactivation' );
5879
5880 if ( $this->is_paying() && ! $this->is_premium() ) {
5881 $this->_admin_notices->add_sticky(
5882 sprintf(
5883 /* translators: %s: License type (e.g. you have a professional license) */
5884 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5885 $this->get_plan_title()
5886 ) . $this->get_complete_upgrade_instructions(),
5887 'plan_upgraded',
5888 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
5889 );
5890 }
5891 }
5892
5893 // Schedule code type changes event.
5894 $this->schedule_install_sync();
5895
5896 /**
5897 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5898 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5899 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5900 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5901 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5902 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5903 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5904 */
5905 $this->unregister_uninstall_hook();
5906
5907 $this->clear_module_main_file_cache();
5908
5909 // Update is_premium of latest version.
5910 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5911 }
5912
5913 #endregion
5914
5915 #----------------------------------------------------------------------------------
5916 #region Add-ons
5917 #----------------------------------------------------------------------------------
5918
5919 /**
5920 * Check if add-on installed and activated on site.
5921 *
5922 * @author Vova Feldman (@svovaf)
5923 * @since 1.0.6
5924 *
5925 * @param string|number $id_or_slug
5926 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5927 *
5928 * @return bool
5929 */
5930 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5931 $this->_logger->entrance();
5932
5933 $addon_id = self::get_module_id( $id_or_slug );
5934 $is_activated = self::has_instance( $addon_id );
5935
5936 if ( ! $is_activated ) {
5937 return false;
5938 }
5939
5940 if ( is_bool( $is_premium ) ) {
5941 // Check if the specified code version is activate.
5942 $addon = $this->get_addon_instance( $addon_id );
5943 $is_activated = ( $is_premium === $addon->is_premium() );
5944 }
5945
5946 return $is_activated;
5947 }
5948
5949 /**
5950 * Check if add-on was connected to install
5951 *
5952 * @author Vova Feldman (@svovaf)
5953 * @since 1.1.7
5954 *
5955 * @param string|number $id_or_slug
5956 *
5957 * @return bool
5958 */
5959 function is_addon_connected( $id_or_slug ) {
5960 $this->_logger->entrance();
5961
5962 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5963
5964 $addon_id = self::get_module_id( $id_or_slug );
5965 $addon = $this->get_addon( $addon_id );
5966 $slug = $addon->slug;
5967 if ( ! isset( $sites[ $slug ] ) ) {
5968 return false;
5969 }
5970
5971 $site = $sites[ $slug ];
5972
5973 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5974
5975 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5976 // The given slug do NOT belong to any of the plugin's add-ons.
5977 return false;
5978 }
5979
5980 return ( is_object( $site ) &&
5981 is_numeric( $site->id ) &&
5982 is_numeric( $site->user_id ) &&
5983 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5984 );
5985 }
5986
5987 /**
5988 * Determines if add-on installed.
5989 *
5990 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5991 *
5992 * @author Vova Feldman (@svovaf)
5993 * @since 1.0.6
5994 *
5995 * @param string|number $id_or_slug
5996 *
5997 * @return bool
5998 */
5999 function is_addon_installed( $id_or_slug ) {
6000 $this->_logger->entrance();
6001
6002 $addon_id = self::get_module_id( $id_or_slug );
6003
6004 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
6005 }
6006
6007 /**
6008 * Get add-on basename.
6009 *
6010 * @author Vova Feldman (@svovaf)
6011 * @since 1.0.6
6012 *
6013 * @param string|number $id_or_slug
6014 *
6015 * @return string
6016 */
6017 function get_addon_basename( $id_or_slug ) {
6018 $addon_id = self::get_module_id( $id_or_slug );
6019
6020 if ( $this->is_addon_activated( $addon_id ) ) {
6021 return self::instance( $addon_id )->get_plugin_basename();
6022 }
6023
6024 $addon = $this->get_addon( $addon_id );
6025 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
6026
6027 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
6028 return $premium_basename;
6029 }
6030
6031 $all_plugins = $this->get_all_plugins();
6032
6033 foreach ( $all_plugins as $basename => $data ) {
6034 if ( $addon->slug === $data['slug'] ||
6035 $addon->premium_slug === $data['slug']
6036 ) {
6037 return $basename;
6038 }
6039 }
6040
6041 $free_basename = "{$addon->slug}/{$addon->slug}.php";
6042
6043 return $free_basename;
6044 }
6045
6046 /**
6047 * Get installed add-ons instances.
6048 *
6049 * @author Vova Feldman (@svovaf)
6050 * @since 1.0.6
6051 *
6052 * @return Freemius[]
6053 */
6054 function get_installed_addons() {
6055 if ( $this->is_addon() ) {
6056 // Add-on cannot have add-ons.
6057 return array();
6058 }
6059
6060 $installed_addons = array();
6061
6062 foreach ( self::$_instances as $instance ) {
6063 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
6064 $installed_addons[] = $instance;
6065 }
6066 }
6067
6068 return $installed_addons;
6069 }
6070
6071 /**
6072 * Check if any add-ons of the plugin are installed.
6073 *
6074 * @author Leo Fajardo (@leorw)
6075 * @since 1.1.1
6076 *
6077 * @return bool
6078 */
6079 function has_installed_addons() {
6080 if ( ! $this->has_addons() ) {
6081 return false;
6082 }
6083
6084 foreach ( self::$_instances as $instance ) {
6085 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
6086 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
6087 return true;
6088 }
6089 }
6090 }
6091
6092 return false;
6093 }
6094
6095 /**
6096 * Tell Freemius that the current plugin is an add-on.
6097 *
6098 * @author Vova Feldman (@svovaf)
6099 * @since 1.0.6
6100 *
6101 * @param number $parent_plugin_id The parent plugin ID
6102 */
6103 function init_addon( $parent_plugin_id ) {
6104 $this->_plugin->parent_plugin_id = $parent_plugin_id;
6105 }
6106
6107 /**
6108 * @author Vova Feldman (@svovaf)
6109 * @since 1.0.6
6110 *
6111 * @return bool
6112 */
6113 function is_addon() {
6114 return (
6115 isset( $this->_plugin->parent_plugin_id ) &&
6116 is_numeric( $this->_plugin->parent_plugin_id )
6117 );
6118 }
6119
6120 /**
6121 * @author Vova Feldman (@svovaf)
6122 * @since 2.3.2
6123 *
6124 * @param number $parent_product_id
6125 *
6126 * @return bool
6127 */
6128 function is_addon_of( $parent_product_id ) {
6129 return (
6130 $this->is_addon() &&
6131 $parent_product_id == $this->_plugin->parent_plugin_id
6132 );
6133 }
6134
6135 /**
6136 * Deactivate add-on if it's premium only and the user does't have a valid license.
6137 *
6138 * @param bool $is_after_trial_cancel
6139 *
6140 * @return bool If add-on was deactivated.
6141 */
6142 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6143 if ( ! $this->has_free_plan() &&
6144 ! $this->has_features_enabled_license() &&
6145 ! $this->_has_premium_license()
6146 ) {
6147 if ( $this->is_registered() ) {
6148 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6149 // if (empty($this->_storage->activation_timestamp) ||
6150 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6151 // ) {
6152 /**
6153 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6154 *
6155 * Retry syncing the user add-on licenses.
6156 */
6157 // Sync licenses.
6158 $this->_sync_licenses();
6159 // }
6160
6161 // Try to activate premium license.
6162 $this->_activate_license( true );
6163 }
6164
6165 if ( ! $this->has_free_plan() &&
6166 ! $this->has_features_enabled_license() &&
6167 ! $this->_has_premium_license()
6168 ) {
6169 // @todo Check if deactivate plugins also call the deactivation hook.
6170
6171 $this->_parent->_admin_notices->add_sticky(
6172 sprintf(
6173 ( $is_after_trial_cancel ?
6174 $this->_parent->get_text_inline(
6175 '%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.',
6176 'addon-trial-cancelled-message'
6177 ) :
6178 $this->_parent->get_text_inline(
6179 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6180 'addon-no-license-message'
6181 )
6182 ),
6183 '<b>' . $this->_plugin->title . '</b>'
6184 ) . ' ' . sprintf(
6185 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6186 $this->_parent->addon_url( $this->_slug ),
6187 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6188 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6189 ),
6190 'no_addon_license_' . $this->_slug,
6191 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6192 ( $is_after_trial_cancel ? 'success' : 'error' )
6193 );
6194
6195 deactivate_plugins( array( $this->_plugin_basename ), true );
6196
6197 return true;
6198 }
6199 }
6200
6201 return false;
6202 }
6203
6204 #endregion
6205
6206 #----------------------------------------------------------------------------------
6207 #region Sandbox
6208 #----------------------------------------------------------------------------------
6209
6210 /**
6211 * Set Freemius into sandbox mode for debugging.
6212 *
6213 * @author Vova Feldman (@svovaf)
6214 * @since 1.0.4
6215 *
6216 * @param string $secret_key
6217 */
6218 function init_sandbox( $secret_key ) {
6219 $this->_plugin->secret_key = $secret_key;
6220
6221 // Update plugin details.
6222 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6223 }
6224
6225 /**
6226 * Check if running payments in sandbox mode.
6227 *
6228 * @author Vova Feldman (@svovaf)
6229 * @since 1.0.4
6230 *
6231 * @return bool
6232 */
6233 function is_payments_sandbox() {
6234 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6235 }
6236
6237 #endregion
6238
6239 /**
6240 * Check if running test vs. live plugin.
6241 *
6242 * @author Vova Feldman (@svovaf)
6243 * @since 1.0.5
6244 *
6245 * @return bool
6246 */
6247 function is_live() {
6248 return $this->_plugin->is_live;
6249 }
6250
6251 /**
6252 * Check if super-admin skipped connection for all sites in the network.
6253 *
6254 * @author Vova Feldman (@svovaf)
6255 * @since 2.0.0
6256 */
6257 function is_network_anonymous() {
6258 if ( ! $this->_is_network_active ) {
6259 return false;
6260 }
6261
6262 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6263
6264 if ( empty( $is_anonymous_ms ) ) {
6265 return false;
6266 }
6267
6268 return $is_anonymous_ms['is'];
6269 }
6270
6271 /**
6272 * Check if super-admin opted-in for all sites in the network.
6273 *
6274 * @author Vova Feldman (@svovaf)
6275 * @since 2.0.0
6276 */
6277 function is_network_connected() {
6278 if ( ! $this->_is_network_active ) {
6279 return false;
6280 }
6281
6282 return $this->_storage->get( 'is_network_connected' );
6283 }
6284
6285 /**
6286 * Check if the user skipped connecting the account with Freemius.
6287 *
6288 * @author Vova Feldman (@svovaf)
6289 * @since 1.0.7
6290 *
6291 * @return bool
6292 */
6293 function is_anonymous() {
6294 if ( ! isset( $this->_is_anonymous ) ) {
6295 if ( $this->is_network_anonymous() ) {
6296 $this->_is_anonymous = true;
6297 } else if ( ! fs_is_network_admin() ) {
6298 if ( ! isset( $this->_storage->is_anonymous ) ) {
6299 // Not skipped.
6300 $this->_is_anonymous = false;
6301 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6302 // For back compatibility, since the variable was boolean before.
6303 $this->_is_anonymous = $this->_storage->is_anonymous;
6304
6305 // Upgrade stored data format to 1.1.3 format.
6306 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6307 } else {
6308 // Version 1.1.3 and later.
6309 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6310 }
6311 }
6312 }
6313
6314 return $this->_is_anonymous;
6315 }
6316
6317 /**
6318 * Check if the user skipped the connection of a specified site.
6319 *
6320 * @author Vova Feldman (@svovaf)
6321 * @since 2.0.0
6322 *
6323 * @param int $blog_id
6324 *
6325 * @return bool
6326 */
6327 function is_anonymous_site( $blog_id = 0 ) {
6328 if ( $this->is_network_anonymous() ) {
6329 return true;
6330 }
6331
6332 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6333
6334 if ( empty( $is_anonymous ) ) {
6335 return false;
6336 }
6337
6338 return $is_anonymous['is'];
6339 }
6340
6341 /**
6342 * Check if user connected his account and install pending email activation.
6343 *
6344 * @author Vova Feldman (@svovaf)
6345 * @since 1.0.7
6346 *
6347 * @return bool
6348 */
6349 function is_pending_activation() {
6350 return $this->_storage->get( 'is_pending_activation', false );
6351 }
6352
6353 /**
6354 * Check if plugin must be WordPress.org compliant.
6355 *
6356 * @since 1.0.7
6357 *
6358 * @return bool
6359 */
6360 function is_org_repo_compliant() {
6361 return $this->_is_org_compliant;
6362 }
6363
6364 #--------------------------------------------------------------------------------
6365 #region WP Cron Common
6366 #--------------------------------------------------------------------------------
6367
6368 /**
6369 * @author Vova Feldman (@svovaf)
6370 * @since 2.0.0
6371 *
6372 * @param string $name Cron name.
6373 *
6374 * @return object
6375 */
6376 private function get_cron_data( $name ) {
6377 $this->_logger->entrance( $name );
6378
6379 /**
6380 * @var object $cron_data
6381 */
6382 return $this->_storage->get( "{$name}_cron", null );
6383 }
6384
6385 /**
6386 * @author Vova Feldman (@svovaf)
6387 * @since 2.0.0
6388 *
6389 * @param string $name Cron name.
6390 */
6391 private function clear_cron_data( $name ) {
6392 $this->_logger->entrance( $name );
6393
6394 $this->_storage->remove( "{$name}_cron" );
6395 }
6396
6397 /**
6398 * @author Vova Feldman (@svovaf)
6399 * @since 2.0.0
6400 *
6401 * @param string $name Cron name.
6402 * @param int $cron_blog_id The cron executing blog ID.
6403 */
6404 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6405 $this->_logger->entrance( $name );
6406
6407 $this->_storage->store( "{$name}_cron", (object) array(
6408 'version' => $this->get_plugin_version(),
6409 'blog_id' => $cron_blog_id,
6410 'sdk_version' => $this->version,
6411 'timestamp' => WP_FS__SCRIPT_START_TIME,
6412 'on' => true,
6413 ) );
6414 }
6415
6416 /**
6417 * Get the cron's executing blog ID.
6418 *
6419 * @author Vova Feldman (@svovaf)
6420 * @since 2.0.0
6421 *
6422 * @param string $name Cron name.
6423 *
6424 * @return int
6425 */
6426 private function get_cron_blog_id( $name ) {
6427 $this->_logger->entrance( $name );
6428
6429 /**
6430 * @var object $cron_data
6431 */
6432 $cron_data = $this->get_cron_data( $name );
6433
6434 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6435 $cron_data->blog_id :
6436 0;
6437 }
6438
6439 /**
6440 * @author Vova Feldman (@svovaf)
6441 * @since 2.0.0
6442 *
6443 * @param string $name Cron name.
6444 *
6445 * @return bool
6446 */
6447 private function is_cron_on( $name ) {
6448 $this->_logger->entrance( $name );
6449
6450 /**
6451 * @var object $cron_data
6452 */
6453 $cron_data = $this->get_cron_data( $name );
6454
6455 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6456 }
6457
6458 /**
6459 * Unix timestamp for previous cron execution or false if never executed.
6460 *
6461 * @author Vova Feldman (@svovaf)
6462 * @since 2.0.0
6463 *
6464 * @param string $name Cron name.
6465 *
6466 * @return int|false
6467 */
6468 private function cron_last_execution( $name ) {
6469 $this->_logger->entrance( $name );
6470
6471 return $this->_storage->get( "{$name}_timestamp" );
6472 }
6473
6474 /**
6475 * Set cron execution time to now.
6476 *
6477 * @author Vova Feldman (@svovaf)
6478 * @since 2.0.0
6479 *
6480 * @param string $name Cron name.
6481 */
6482 private function set_cron_execution_timestamp( $name ) {
6483 $this->_logger->entrance( $name );
6484
6485 $this->_storage->store( "{$name}_timestamp", time() );
6486 }
6487
6488 /**
6489 * Sets the keepalive time to now.
6490 *
6491 * @author Leo Fajardo (@leorw)
6492 * @since 2.2.3
6493 *
6494 * @param bool|null $use_network_level_storage
6495 */
6496 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6497 $this->_logger->entrance();
6498
6499 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6500 }
6501
6502 /**
6503 * Check if cron was executed in the last $period of seconds.
6504 *
6505 * @author Vova Feldman (@svovaf)
6506 * @since 2.0.0
6507 *
6508 * @param string $name Cron name.
6509 * @param int $period In seconds
6510 *
6511 * @return bool
6512 */
6513 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6514 $this->_logger->entrance( $name );
6515
6516 $last_execution = $this->cron_last_execution( $name );
6517
6518 if ( ! is_numeric( $last_execution ) ) {
6519 return false;
6520 }
6521
6522 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6523 }
6524
6525 /**
6526 * WP Cron is executed on a site level. When running in a multisite network environment
6527 * with the network integration activated, for optimization reasons, we are consolidating
6528 * the installs data sync cron to be executed only from a single site.
6529 *
6530 * @author Vova Feldman (@svovaf)
6531 * @since 2.0.0
6532 *
6533 * @param int $except_blog_id Target any except the excluded blog ID.
6534 *
6535 * @return int
6536 */
6537 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6538 if ( ! is_multisite() ) {
6539 return 0;
6540 }
6541
6542 if ( $this->_is_network_active &&
6543 is_numeric( $this->_storage->network_install_blog_id ) &&
6544 $except_blog_id != $this->_storage->network_install_blog_id &&
6545 self::is_site_active( $this->_storage->network_install_blog_id )
6546 ) {
6547 // Try to run cron from the main network blog.
6548 $install = $this->get_install_by_blog_id( $this->_storage->network_install_blog_id );
6549
6550 if ( is_object( $install ) &&
6551 ( $this->is_premium() || $install->is_tracking_allowed() )
6552 ) {
6553 return $this->_storage->network_install_blog_id;
6554 }
6555 }
6556
6557 // Get first opted-in blog ID with active tracking.
6558 $installs = $this->get_blog_install_map();
6559 foreach ( $installs as $blog_id => $install ) {
6560 if ( $except_blog_id != $blog_id &&
6561 self::is_site_active( $blog_id ) &&
6562 ( $this->is_premium() || $install->is_tracking_allowed() )
6563 ) {
6564 return $blog_id;
6565 }
6566 }
6567
6568 return 0;
6569 }
6570
6571 /**
6572 * @author Vova Feldman (@svovaf)
6573 * @since 2.0.0
6574 *
6575 * @param string $name Cron name.
6576 * @param string $action_tag Callback action tag.
6577 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6578 */
6579 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6580 $this->_logger->entrance( $name );
6581
6582 if ( ! $this->is_cron_on( $name ) ) {
6583 return;
6584 }
6585
6586 $clear_cron = true;
6587 if ( ! $is_network_clear && $this->_is_network_active ) {
6588 $installs = $this->get_blog_install_map();
6589
6590 foreach ( $installs as $blog_id => $install ) {
6591 /**
6592 * @var FS_Site $install
6593 */
6594 if ( $install->is_tracking_allowed() ) {
6595 $clear_cron = false;
6596 break;
6597 }
6598 }
6599 }
6600
6601 if ( ! $clear_cron ) {
6602 return;
6603 }
6604
6605 /**
6606 * @var object $cron_data
6607 */
6608 $cron_data = $this->get_cron_data( $name );
6609
6610 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6611 $cron_data->blog_id :
6612 0;
6613
6614 $this->clear_cron_data( $name );
6615
6616 if ( 0 < $cron_blog_id ) {
6617 switch_to_blog( $cron_blog_id );
6618 }
6619
6620 if ( empty( $action_tag ) ) {
6621 $action_tag = $name;
6622 }
6623
6624 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6625
6626 if ( 0 < $cron_blog_id ) {
6627 restore_current_blog();
6628 }
6629 }
6630
6631 /**
6632 * Unix timestamp for next cron execution or false if not scheduled.
6633 *
6634 * @author Vova Feldman (@svovaf)
6635 * @since 2.0.0
6636 *
6637 * @param string $name Cron name.
6638 * @param string $action_tag Callback action tag.
6639 *
6640 * @return int|false
6641 */
6642 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6643 $this->_logger->entrance( $name );
6644
6645 if ( ! $this->is_cron_on( $name ) ) {
6646 return false;
6647 }
6648
6649 /**
6650 * @var object $cron_data
6651 */
6652 $cron_data = $this->get_cron_data( $name );
6653
6654 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6655 $cron_data->blog_id :
6656 0;
6657
6658 if ( 0 < $cron_blog_id ) {
6659 switch_to_blog( $cron_blog_id );
6660 }
6661
6662 if ( empty( $action_tag ) ) {
6663 $action_tag = $name;
6664 }
6665
6666 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6667
6668 if ( 0 < $cron_blog_id ) {
6669 restore_current_blog();
6670 }
6671
6672 return $next_scheduled;
6673 }
6674
6675 /**
6676 * @author Vova Feldman (@svovaf)
6677 * @since 2.0.0
6678 *
6679 * @param string $name Cron name.
6680 * @param string $action_tag Callback action tag.
6681 * @param string $recurrence 'single' or 'daily'.
6682 * @param int $start_at Defaults to now.
6683 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6684 * @param int $except_blog_id Target any except the excluded blog ID.
6685 */
6686 private function schedule_cron(
6687 $name,
6688 $action_tag = '',
6689 $recurrence = 'single',
6690 $start_at = WP_FS__SCRIPT_START_TIME,
6691 $randomize_start = true,
6692 $except_blog_id = 0
6693 ) {
6694 $this->_logger->entrance( $name );
6695
6696 $this->clear_cron( $name, $action_tag, true );
6697
6698 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6699
6700 if ( is_multisite() && 0 == $cron_blog_id ) {
6701 // Don't schedule cron since couldn't find a target blog.
6702 return;
6703 }
6704
6705 if ( 0 < $cron_blog_id ) {
6706 switch_to_blog( $cron_blog_id );
6707 }
6708
6709 if ( 'daily' === $recurrence ) {
6710 if ( $randomize_start ) {
6711 // Schedule first sync with a random 12 hour time range from now.
6712 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6713 }
6714
6715 // Schedule daily WP cron.
6716 wp_schedule_event(
6717 $start_at,
6718 'daily',
6719 $this->get_action_tag( $action_tag )
6720 );
6721 } else if ( 'single' === $recurrence ) {
6722 // Schedule single cron.
6723 wp_schedule_single_event(
6724 $start_at,
6725 $this->get_action_tag( $action_tag )
6726 );
6727 }
6728
6729 $this->set_cron_data( $name, $cron_blog_id );
6730
6731 if ( 0 < $cron_blog_id ) {
6732 restore_current_blog();
6733 }
6734 }
6735
6736 /**
6737 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6738 * that doesn't halt page loading.
6739 *
6740 * @author Vova Feldman (@svovaf)
6741 * @since 2.0.0
6742 *
6743 * @param string $name Cron name.
6744 * @param callable $callable The function that should be executed.
6745 */
6746 private function execute_cron( $name, $callable ) {
6747 $this->_logger->entrance( $name );
6748
6749 // Store the last time data sync was executed.
6750 $this->set_cron_execution_timestamp( $name );
6751
6752 // Check if API is temporary down.
6753 if ( FS_Api::is_temporary_down() ) {
6754 return;
6755 }
6756
6757 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6758
6759 $users_2_blog_ids = array();
6760
6761 if ( ! is_multisite() ) {
6762 // Add dummy blog.
6763 $users_2_blog_ids[0] = array( 0 );
6764 } else {
6765 $installs = $this->get_blog_install_map();
6766 foreach ( $installs as $blog_id => $install ) {
6767 if ( $this->is_premium() || $install->is_tracking_allowed() ) {
6768 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6769 $users_2_blog_ids[ $install->user_id ] = array();
6770 }
6771
6772 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6773 }
6774 }
6775 }
6776
6777 $current_blog_id = get_current_blog_id();
6778
6779 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6780 if ( 0 < $blog_ids[0] ) {
6781 $this->switch_to_blog( $blog_ids[0] );
6782 }
6783
6784 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6785
6786 foreach ( $blog_ids as $blog_id ) {
6787 $this->do_action( "after_{$name}_cron", $blog_id );
6788 }
6789 }
6790
6791 if ( is_multisite() ) {
6792 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6793
6794 $this->do_action( "after_{$name}_cron_multisite" );
6795 }
6796 }
6797
6798 #endregion
6799
6800 #----------------------------------------------------------------------------------
6801 #region Daily Sync Cron
6802 #----------------------------------------------------------------------------------
6803
6804
6805 /**
6806 * @author Vova Feldman (@svovaf)
6807 * @since 2.0.0
6808 *
6809 * @return bool
6810 */
6811 private function is_sync_cron_scheduled() {
6812 return $this->is_cron_on( 'sync' );
6813 }
6814
6815 /**
6816 * Get the sync cron's executing blog ID.
6817 *
6818 * @author Vova Feldman (@svovaf)
6819 * @since 2.0.0
6820 *
6821 * @return int
6822 */
6823 private function get_sync_cron_blog_id() {
6824 return $this->get_cron_blog_id( 'sync' );
6825 }
6826
6827 /**
6828 * @author Vova Feldman (@svovaf)
6829 * @since 1.1.7.3
6830 */
6831 private function run_manual_sync() {
6832 self::require_pluggable_essentials();
6833
6834 if ( ! $this->is_user_admin() ) {
6835 return;
6836 }
6837
6838 // Run manual sync.
6839 $this->_sync_cron();
6840
6841 // Reschedule next cron to run 24 hours from now (performance optimization).
6842 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6843 }
6844
6845 /**
6846 * Data sync cron job. Replaces the background sync non blocking HTTP request
6847 * that doesn't halt page loading.
6848 *
6849 * @author Vova Feldman (@svovaf)
6850 * @since 1.1.7.3
6851 * @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.
6852 */
6853 function _sync_cron() {
6854 $this->_logger->entrance();
6855
6856 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6857 }
6858
6859 /**
6860 * The actual data sync cron logic.
6861 *
6862 * @author Vova Feldman (@svovaf)
6863 * @since 2.0.0
6864 *
6865 * @param int[] $blog_ids
6866 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6867 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6868 * updates for a single site in case `execute_cron` has switched to a different blog.
6869 */
6870 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6871 if ( $this->is_registered() ) {
6872 if ( $this->has_paid_plan() ) {
6873 // Initiate background plan sync.
6874 $this->_sync_license( true, false, $current_blog_id );
6875
6876 if ( $this->is_paying() ) {
6877 // Check for premium plugin updates.
6878 $this->check_updates( true );
6879 }
6880 } else {
6881 // Sync install(s) (only if something changed locally).
6882 if ( 1 < count( $blog_ids ) ) {
6883 $this->sync_installs();
6884 } else {
6885 $this->sync_install();
6886 }
6887
6888 $this->maybe_sync_install_user();
6889 }
6890 }
6891 }
6892
6893 /**
6894 * Check if sync was executed in the last $period of seconds.
6895 *
6896 * @author Vova Feldman (@svovaf)
6897 * @since 1.1.7.3
6898 *
6899 * @param int $period In seconds
6900 *
6901 * @return bool
6902 */
6903 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6904 return $this->is_cron_executed( 'sync', $period );
6905 }
6906
6907 /**
6908 * @author Vova Feldman (@svovaf)
6909 * @since 1.1.7.3
6910 *
6911 * @return bool
6912 */
6913 private function is_sync_cron_on() {
6914 return $this->is_cron_on( 'sync' );
6915 }
6916
6917 /**
6918 * @author Vova Feldman (@svovaf)
6919 * @since 1.1.7.3
6920 *
6921 * @param int $start_at Defaults to now.
6922 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6923 * @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.
6924 */
6925 private function schedule_sync_cron(
6926 $start_at = WP_FS__SCRIPT_START_TIME,
6927 $randomize_start = true,
6928 $except_blog_id = 0
6929 ) {
6930 $this->schedule_cron(
6931 'sync',
6932 'data_sync',
6933 'daily',
6934 $start_at,
6935 $randomize_start,
6936 $except_blog_id
6937 );
6938 }
6939
6940 /**
6941 * Add the actual sync function to the cron job hook.
6942 *
6943 * @author Vova Feldman (@svovaf)
6944 * @since 1.1.7.3
6945 */
6946 private function hook_callback_to_sync_cron() {
6947 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6948 }
6949
6950 /**
6951 * @author Vova Feldman (@svovaf)
6952 * @since 1.1.7.3
6953 *
6954 * @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.
6955 */
6956 private function clear_sync_cron( $is_network_clear = false ) {
6957 $this->_logger->entrance();
6958
6959 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6960 }
6961
6962 /**
6963 * Unix timestamp for next sync cron execution or false if not scheduled.
6964 *
6965 * @author Vova Feldman (@svovaf)
6966 * @since 1.1.7.3
6967 *
6968 * @return int|false
6969 */
6970 function next_sync_cron() {
6971 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6972 }
6973
6974 /**
6975 * Unix timestamp for previous sync cron execution or false if never executed.
6976 *
6977 * @author Vova Feldman (@svovaf)
6978 * @since 1.1.7.3
6979 *
6980 * @return int|false
6981 */
6982 function last_sync_cron() {
6983 return $this->cron_last_execution( 'sync' );
6984 }
6985
6986 #endregion Daily Sync Cron ------------------------------------------------------------------
6987
6988 #----------------------------------------------------------------------------------
6989 #region Async Install Sync
6990 #----------------------------------------------------------------------------------
6991
6992 /**
6993 * @author Vova Feldman (@svovaf)
6994 * @since 1.1.7.3
6995 *
6996 * @return bool
6997 */
6998 private function is_install_sync_scheduled() {
6999 return $this->is_cron_on( 'install_sync' );
7000 }
7001
7002 /**
7003 * Get the sync cron's executing blog ID.
7004 *
7005 * @author Vova Feldman (@svovaf)
7006 * @since 2.0.0
7007 *
7008 * @return int
7009 */
7010 private function get_install_sync_cron_blog_id() {
7011 return $this->get_cron_blog_id( 'install_sync' );
7012 }
7013
7014 /**
7015 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
7016 *
7017 * @author Vova Feldman (@svovaf)
7018 * @since 1.1.7.3
7019 *
7020 * @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.
7021 */
7022 private function schedule_install_sync( $except_blog_id = 0 ) {
7023 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
7024 }
7025
7026 /**
7027 * Unix timestamp for previous install sync cron execution or false if never executed.
7028 *
7029 * @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.
7030 *
7031 * @author Vova Feldman (@svovaf)
7032 * @since 1.1.7.3
7033 *
7034 * @return int|false
7035 */
7036 function last_install_sync() {
7037 return $this->cron_last_execution( 'install_sync' );
7038 }
7039
7040 /**
7041 * Unix timestamp for next install sync cron execution or false if not scheduled.
7042 *
7043 * @author Vova Feldman (@svovaf)
7044 * @since 1.1.7.3
7045 *
7046 * @return int|false
7047 */
7048 function next_install_sync() {
7049 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
7050 }
7051
7052 /**
7053 * Add the actual install sync function to the cron job hook.
7054 *
7055 * @author Vova Feldman (@svovaf)
7056 * @since 1.1.7.3
7057 */
7058 private function hook_callback_to_install_sync() {
7059 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
7060 }
7061
7062 /**
7063 * @author Vova Feldman (@svovaf)
7064 * @since 1.1.7.3
7065 *
7066 * @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.
7067 */
7068 private function clear_install_sync_cron( $is_network_clear = false ) {
7069 $this->_logger->entrance();
7070
7071 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
7072 }
7073
7074 /**
7075 * @author Vova Feldman (@svovaf)
7076 * @since 1.1.7.3
7077 * @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.
7078 */
7079 public function _run_sync_install() {
7080 $this->_logger->entrance();
7081
7082 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
7083 }
7084
7085 /**
7086 * The actual install(s) sync cron logic.
7087 *
7088 * @author Vova Feldman (@svovaf)
7089 * @since 2.0.0
7090 *
7091 * @param int[] $blog_ids
7092 * @param int|null $current_blog_id
7093 */
7094 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
7095 if ( $this->is_registered() ) {
7096 if ( 1 < count( $blog_ids ) ) {
7097 $this->sync_installs( array(), true );
7098 } else {
7099 $this->sync_install( array(), true );
7100 }
7101
7102 $this->maybe_sync_install_user();
7103 }
7104 }
7105
7106 #endregion Async Install Sync ------------------------------------------------------------------
7107
7108 /**
7109 * Show a notice that activation is currently pending.
7110 *
7111 * @author Vova Feldman (@svovaf)
7112 * @since 1.0.7
7113 *
7114 * @param bool|string $email
7115 * @param bool $is_pending_trial Since 1.2.1.5
7116 */
7117 function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) {
7118 if ( ! is_string( $email ) ) {
7119 $current_user = self::_get_current_wp_user();
7120 $email = $current_user->user_email;
7121 }
7122
7123 $this->_admin_notices->add_sticky(
7124 sprintf(
7125 $this->get_text_inline( 'You should receive an activation email for %s to your mailbox at %s. Please make sure you click the activation button in that email to %s.', 'pending-activation-message' ),
7126 '<b>' . $this->get_plugin_name() . '</b>',
7127 '<b>' . $email . '</b>',
7128 ( $is_pending_trial ?
7129 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7130 $this->get_text_inline( 'complete the install', 'complete-the-install' ) )
7131 ),
7132 'activation_pending',
7133 'Thanks!'
7134 );
7135 }
7136
7137 /**
7138 * Check if currently in plugin activation.
7139 *
7140 * @author Vova Feldman (@svovaf)
7141 * @since 1.1.4
7142 *
7143 * @return bool
7144 */
7145 function is_plugin_activation() {
7146 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7147
7148 return !empty($result);
7149 }
7150
7151 /**
7152 *
7153 * NOTE: admin_menu action executed before admin_init.
7154 *
7155 * @author Vova Feldman (@svovaf)
7156 * @since 1.0.7
7157 */
7158 function _admin_init_action() {
7159 $is_migration = $this->is_migration();
7160
7161 /**
7162 * Automatically redirect to connect/activation page after plugin activation.
7163 *
7164 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7165 */
7166 if ( $this->is_plugin_activation() ) {
7167 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7168
7169 if ( isset( $_GET['activate-multi'] ) ) {
7170 /**
7171 * Don't redirect if activating multiple plugins at once (bulk activation).
7172 */
7173 } else if ( ! $is_migration ) {
7174 $this->_redirect_on_activation_hook();
7175 return;
7176 }
7177 }
7178
7179 if ( $is_migration ) {
7180 return;
7181 }
7182
7183 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7184 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7185
7186 $this->skip_connection( null, fs_is_network_admin() );
7187
7188 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7189 }
7190
7191 if ( $this->is_network_activation_mode() &&
7192 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7193 ) {
7194 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7195
7196 $this->delegate_connection();
7197
7198 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7199 }
7200
7201 $this->_add_upgrade_action_link();
7202
7203 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7204 (
7205 ( true === $this->_storage->require_license_activation ) ||
7206 // Not registered nor anonymous.
7207 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7208 // OR, network level and in network upgrade mode.
7209 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7210 )
7211 ) {
7212 if ( ! $this->is_pending_activation() ) {
7213 if ( ! $this->is_activation_page() ) {
7214 /**
7215 * If a user visits any other admin page before activating the premium-only theme with a valid
7216 * license, reactivate the previous theme.
7217 *
7218 * @author Leo Fajardo (@leorw)
7219 * @since 1.2.2
7220 */
7221 if ( $this->is_theme() &&
7222 ! $this->has_settings_menu() &&
7223 ! isset( $_REQUEST['fs_action'] ) &&
7224 $this->can_activate_previous_theme()
7225 ) {
7226 if ( $this->is_only_premium() ) {
7227 $this->activate_previous_theme();
7228 return;
7229 }
7230
7231 if ( true === $this->_storage->require_license_activation ) {
7232 $this->_storage->require_license_activation = false;
7233 }
7234 }
7235
7236 if ( ! fs_is_network_admin() &&
7237 $this->is_network_activation_mode() &&
7238 ! $this->is_delegated_connection()
7239 ) {
7240 return;
7241 }
7242
7243 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7244 if ( ! $this->_anonymous_mode &&
7245 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7246 // Show notice for new plugin installations.
7247 $this->_admin_notices->add(
7248 sprintf(
7249 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7250 sprintf( '<b><a href="%s">%s</a></b>',
7251 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7252 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7253 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7254 )
7255 ),
7256 '',
7257 'update-nag'
7258 );
7259 }
7260 } else {
7261 if ( $this->should_add_sticky_optin_notice() ) {
7262 $this->add_sticky_optin_admin_notice();
7263 }
7264
7265 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7266 // Don't show admin nag if plugin update.
7267 wp_enqueue_script( 'wp-pointer' );
7268 wp_enqueue_style( 'wp-pointer' );
7269
7270 $this->_enqueue_connect_essentials();
7271
7272 add_action( 'admin_print_footer_scripts', array(
7273 $this,
7274 '_add_connect_pointer_script'
7275 ) );
7276 }
7277 }
7278 }
7279 }
7280
7281 if ( $this->show_opt_in_on_themes_page() &&
7282 $this->is_activation_page()
7283 ) {
7284 $this->_show_theme_activation_optin_dialog();
7285 }
7286 }
7287 }
7288
7289 /**
7290 * @author Vova Feldman (@svovaf)
7291 * @since 2.0.0
7292 *
7293 * @return bool
7294 */
7295 private function should_add_sticky_optin_notice() {
7296 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7297 return false;
7298 }
7299
7300 if ( fs_is_network_admin() ) {
7301 if ( ! $this->_is_network_active ) {
7302 return false;
7303 }
7304
7305 if ( ! $this->is_network_activation_mode() ) {
7306 return false;
7307 }
7308
7309 return ! isset( $this->_storage->sticky_optin_added_ms );
7310 }
7311
7312 if ( ! $this->is_activation_mode() ) {
7313 return false;
7314 }
7315
7316 // If running from a blog admin and delegated the connection.
7317 return ! isset( $this->_storage->sticky_optin_added );
7318 }
7319
7320 /**
7321 * @author Leo Fajardo (@leorw)
7322 * @since 2.0.0
7323 */
7324 private function add_sticky_optin_admin_notice() {
7325 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7326 $this->_storage->sticky_optin_added = true;
7327 } else {
7328 $this->_storage->sticky_optin_added_ms = true;
7329 }
7330
7331 // Show notice for new plugin installations.
7332 $this->_admin_notices->add_sticky(
7333 sprintf(
7334 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7335 $this->_module_type,
7336 sprintf( '<b><a href="%s">%s</a></b>',
7337 $this->get_activation_url(),
7338 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7339 )
7340 ),
7341 'connect_account',
7342 '',
7343 'update-nag'
7344 );
7345 }
7346
7347 /**
7348 * Enqueue connect requires scripts and styles.
7349 *
7350 * @author Vova Feldman (@svovaf)
7351 * @since 1.1.4
7352 */
7353 function _enqueue_connect_essentials() {
7354 wp_enqueue_script( 'jquery' );
7355 wp_enqueue_script( 'json2' );
7356
7357 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7358 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7359
7360 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
7361 }
7362
7363 /**
7364 * Add connect / opt-in pointer.
7365 *
7366 * @author Vova Feldman (@svovaf)
7367 * @since 1.1.4
7368 */
7369 function _add_connect_pointer_script() {
7370 $vars = array( 'id' => $this->_module_id );
7371 $pointer_content = fs_get_template( 'connect.php', $vars );
7372 ?>
7373 <script type="text/javascript">// <![CDATA[
7374 jQuery(document).ready(function ($) {
7375 if ('undefined' !== typeof(jQuery().pointer)) {
7376
7377 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7378
7379 if (element.length > 0) {
7380 var optin = $(element).pointer($.extend(true, {}, {
7381 content : <?php echo json_encode( $pointer_content ) ?>,
7382 position : {
7383 edge : 'left',
7384 align: 'center'
7385 },
7386 buttons : function () {
7387 // Don't show pointer buttons.
7388 return '';
7389 },
7390 pointerWidth: 482
7391 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7392
7393 <?php
7394 echo $this->apply_filters( 'optin_pointer_execute', "
7395
7396 optin.pointer('open');
7397
7398 // Tag the opt-in pointer with custom class.
7399 $('.wp-pointer #fs_connect')
7400 .parents('.wp-pointer.wp-pointer-top')
7401 .addClass('fs-opt-in-pointer');
7402
7403 ", 'element', 'optin' ) ?>
7404 }
7405 }
7406 });
7407 // ]]></script>
7408 <?php
7409 }
7410
7411 /**
7412 * Return current page's URL.
7413 *
7414 * @author Vova Feldman (@svovaf)
7415 * @since 1.0.7
7416 *
7417 * @return string
7418 */
7419 function current_page_url() {
7420 $url = 'http';
7421
7422 if ( isset( $_SERVER["HTTPS"] ) ) {
7423 if ( $_SERVER["HTTPS"] == "on" ) {
7424 $url .= "s";
7425 }
7426 }
7427 $url .= "://";
7428 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7429 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7430 } else {
7431 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7432 }
7433
7434 return esc_url( $url );
7435 }
7436
7437 /**
7438 * Check if the current page is the plugin's main admin settings page.
7439 *
7440 * @author Vova Feldman (@svovaf)
7441 * @since 1.0.7
7442 *
7443 * @return bool
7444 */
7445 function _is_plugin_page() {
7446 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7447 fs_is_plugin_page( $this->_slug );
7448 }
7449
7450 /* Events
7451 ------------------------------------------------------------------------------------------------------------------*/
7452 /**
7453 * Delete site install from Database.
7454 *
7455 * @author Vova Feldman (@svovaf)
7456 * @since 1.0.1
7457 *
7458 * @param bool $store
7459 * @param int|null $blog_id Since 2.0.0
7460 *
7461 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7462 */
7463 function _delete_site( $store = true, $blog_id = null ) {
7464 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7465 }
7466
7467 /**
7468 * Delete site install from Database.
7469 *
7470 * @author Vova Feldman (@svovaf)
7471 * @since 1.2.2.7
7472 *
7473 * @param string $slug
7474 * @param string $module_type
7475 * @param bool $store
7476 * @param int|null $blog_id Since 2.0.0
7477 *
7478 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7479 */
7480 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7481 $sites = self::get_all_sites( $module_type, $blog_id );
7482
7483 $install_id = false;
7484
7485 if ( isset( $sites[ $slug ] ) ) {
7486 if ( is_object( $sites[ $slug ] ) ) {
7487 $install_id = $sites[ $slug ]->id;
7488 }
7489
7490 unset( $sites[ $slug ] );
7491
7492 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7493 }
7494
7495 return $install_id;
7496 }
7497
7498 /**
7499 * Delete user.
7500 *
7501 * @author Vova Feldman (@svovaf)
7502 * @since 2.0.0
7503 *
7504 * @param number $user_id
7505 * @param bool $store
7506 *
7507 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7508 */
7509 private static function delete_user( $user_id, $store = true ) {
7510 $users = self::get_all_users();
7511
7512 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7513 return false;
7514 }
7515
7516 unset( $users[ $user_id ] );
7517
7518 self::$_accounts->set_option( 'users', $users, $store );
7519
7520 return $user_id;
7521 }
7522
7523 /**
7524 * Delete plugin's plans information.
7525 *
7526 * @param bool $store Flush to Database if true.
7527 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7528 *
7529 * @author Vova Feldman (@svovaf)
7530 * @since 1.0.9
7531 */
7532 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7533 $this->_logger->entrance();
7534
7535 $plans = self::get_all_plans( $this->_module_type );
7536
7537 $plans_to_keep = array();
7538
7539 if ( $keep_associated_plans ) {
7540 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7541 foreach ( $plans_ids_to_keep as $plan_id ) {
7542 $plan = self::_get_plan_by_id( $plan_id );
7543 if ( is_object( $plan ) ) {
7544 $plans_to_keep[] = self::_encrypt_entity( $plan );
7545 }
7546 }
7547 }
7548
7549 if ( ! empty( $plans_to_keep ) ) {
7550 $plans[ $this->_slug ] = $plans_to_keep;
7551 } else {
7552 unset( $plans[ $this->_slug ] );
7553 }
7554
7555 $this->set_account_option( 'plans', $plans, $store );
7556 }
7557
7558 /**
7559 * Delete all plugin licenses.
7560 *
7561 * @author Vova Feldman (@svovaf)
7562 * @since 1.0.9
7563 *
7564 * @param bool $store
7565 */
7566 private function _delete_licenses( $store = true ) {
7567 $this->_logger->entrance();
7568
7569 $all_licenses = self::get_all_licenses();
7570
7571 unset( $all_licenses[ $this->_module_id ] );
7572
7573 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7574 }
7575
7576 /**
7577 * Check if Freemius was added on new plugin installation.
7578 *
7579 * @author Vova Feldman (@svovaf)
7580 * @since 1.1.5
7581 *
7582 * @return bool
7583 */
7584 function is_plugin_new_install() {
7585 return isset( $this->_storage->is_plugin_new_install ) &&
7586 $this->_storage->is_plugin_new_install;
7587 }
7588
7589 /**
7590 * Check if it's the first plugin release that is running Freemius.
7591 *
7592 * @author Vova Feldman (@svovaf)
7593 * @since 1.2.1.5
7594 *
7595 * @return bool
7596 */
7597 function is_first_freemius_powered_version() {
7598 return empty( $this->_storage->plugin_last_version );
7599 }
7600
7601 /**
7602 * @author Leo Fajardo (@leorw)
7603 * @since 1.2.2
7604 *
7605 * @return bool|string
7606 */
7607 private function get_previous_theme_slug() {
7608 return isset( $this->_storage->previous_theme ) ?
7609 $this->_storage->previous_theme :
7610 false;
7611 }
7612
7613 /**
7614 * @author Leo Fajardo (@leorw)
7615 * @since 1.2.2
7616 *
7617 * @return string
7618 */
7619 private function can_activate_previous_theme() {
7620 $slug = $this->get_previous_theme_slug();
7621 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7622 $theme_instance = wp_get_theme( $slug );
7623
7624 return $theme_instance->exists();
7625 }
7626
7627 return false;
7628 }
7629
7630 /**
7631 * @author Leo Fajardo (@leorw)
7632 * @since 1.2.2
7633 */
7634 private function activate_previous_theme() {
7635 switch_theme( $this->get_previous_theme_slug() );
7636 unset( $this->_storage->previous_theme );
7637
7638 global $pagenow;
7639 if ( 'themes.php' === $pagenow ) {
7640 /**
7641 * Refresh the active theme information.
7642 *
7643 * @author Leo Fajardo (@leorw)
7644 * @since 1.2.2
7645 */
7646 fs_redirect( $this->admin_url( $pagenow ) );
7647 }
7648 }
7649
7650 /**
7651 * @author Leo Fajardo (@leorw)
7652 * @since 1.2.2
7653 *
7654 * @return string
7655 */
7656 function get_previous_theme_activation_url() {
7657 if ( ! $this->can_activate_previous_theme() ) {
7658 return '';
7659 }
7660
7661 /**
7662 * Activation URL
7663 *
7664 * @author Leo Fajardo (@leorw)
7665 * @since 1.2.2
7666 */
7667 return wp_nonce_url(
7668 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7669 'switch-theme_' . $this->get_previous_theme_slug()
7670 );
7671 }
7672
7673 /**
7674 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7675 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7676 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7677 * theme doesn't exist, then there will be no close button.
7678 *
7679 * @author Leo Fajardo (@leorw)
7680 * @since 1.2.2
7681 *
7682 * @param string $slug_or_name Old theme's slug or name.
7683 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7684 */
7685 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7686 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7687 $old_theme->get_stylesheet() :
7688 $slug_or_name;
7689
7690 $this->_activate_plugin_event_hook();
7691 }
7692
7693 /**
7694 * Plugin activated hook.
7695 *
7696 * @author Vova Feldman (@svovaf)
7697 * @since 1.0.1
7698 *
7699 * @uses FS_Api
7700 */
7701 function _activate_plugin_event_hook() {
7702 $this->_logger->entrance( 'slug = ' . $this->_slug );
7703
7704 if ( ! $this->is_user_admin() ) {
7705 return;
7706 }
7707
7708 $this->unregister_uninstall_hook();
7709
7710 // Clear API cache on activation.
7711 FS_Api::clear_cache();
7712
7713 $is_premium_version_activation = $this->is_plugin() ?
7714 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7715 $this->is_premium();
7716
7717 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7718
7719 if ( $this->is_plugin() ) {
7720 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7721 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7722 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7723 $other_version_basename = $is_premium_version_activation ?
7724 $this->_free_plugin_basename :
7725 $this->premium_plugin_basename();
7726
7727 if ( ! $this->_is_network_active ) {
7728 /**
7729 * Themes are always network activated, but the ACTUAL activation is per site.
7730 *
7731 * During the activation, the plugin isn't yet active, therefore,
7732 * _is_network_active will be set to false even if it's a network level
7733 * activation. So we need to fix that by looking at the is_network_admin() value.
7734 *
7735 * @author Vova Feldman
7736 */
7737 $this->_is_network_active = (
7738 $this->_is_multisite_integrated &&
7739 fs_is_network_admin()
7740 );
7741 }
7742
7743 /**
7744 * If the other module version is active, deactivate it.
7745 *
7746 * is_plugin_active() checks if the plugin is active on the site or the network level and
7747 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7748 *
7749 * @author Leo Fajardo (@leorw)
7750 * @since 1.2.2
7751 */
7752 if (
7753 is_plugin_active( $other_version_basename ) &&
7754 $this->apply_filters( 'deactivate_on_activation', true )
7755 ) {
7756 deactivate_plugins( $other_version_basename );
7757 }
7758 }
7759
7760 if ( $this->is_registered() ) {
7761 if ( $is_premium_version_activation ) {
7762 $this->reconnect_locally();
7763 }
7764
7765
7766 // Schedule re-activation event and sync.
7767 // $this->sync_install( array(), true );
7768 $this->schedule_install_sync();
7769
7770 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7771 if ( $is_premium_version_activation ) {
7772 $this->_admin_notices->add(
7773 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7774 $this->get_text_x_inline( 'W00t',
7775 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7776 );
7777 }
7778 } else if ( $this->is_anonymous() ) {
7779 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7780 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7781 $network = true;
7782 } else {
7783 $plugin_version = $this->_storage->is_anonymous['version'];
7784 $network = false;
7785 }
7786
7787 /**
7788 * Reset "skipped" click cache on the following:
7789 * 1. Freemius DEV mode.
7790 * 2. WordPress DEBUG mode.
7791 * 3. If a plugin and the user skipped the exact same version before.
7792 *
7793 * @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).
7794 *
7795 * @todo 4. If explicitly asked to retry after every activation.
7796 */
7797 if ( WP_FS__DEV_MODE ||
7798 (
7799 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7800 $this->get_plugin_version() == $plugin_version
7801 )
7802 ) {
7803 $this->reset_anonymous_mode( $network );
7804 }
7805 }
7806
7807 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7808
7809 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7810 /**
7811 * When activating an add-on, try to also activate a license.
7812 *
7813 * @author Leo Fajardo (@leorw)
7814 * @since 2.3.0
7815 */
7816 if ( ! $this->_is_network_active ) {
7817 $this->maybe_activate_addon_license();
7818 } else {
7819 $this->maybe_network_activate_addon_license();
7820 }
7821
7822 /**
7823 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7824 *
7825 * @author Leo Fajardo (@leorw)
7826 * @since 2.3.0
7827 */
7828 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7829
7830 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7831 $this->_storage->require_license_activation = false;
7832 }
7833 }
7834
7835 if (
7836 $is_premium_version_activation &&
7837 (
7838 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7839 (
7840 $this->is_registered() &&
7841 ! $is_trial_or_has_features_enabled_license
7842 )
7843 )
7844 ) {
7845 $this->_storage->require_license_activation = true;
7846 }
7847
7848 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7849 /**
7850 * If no previous version of plugin's version exist, it means that it's either
7851 * the first time that the plugin installed on the site, or the plugin was installed
7852 * before but didn't have Freemius integrated.
7853 *
7854 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7855 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7856 * only after immediate activation.
7857 *
7858 * @since 1.1.4
7859 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7860 */
7861 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7862 }
7863
7864 /**
7865 * Also flush when activating the premium version so that even if Freemius was off before, the API
7866 * connectivity test can be run again.
7867 *
7868 * @author Leo Fajardo (@leorw)
7869 * @since 2.2.3.1
7870 */
7871 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7872
7873 if ( ! $this->_anonymous_mode &&
7874 $has_api_connectivity &&
7875 ! $this->_isAutoInstall
7876 ) {
7877 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7878 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7879 }
7880
7881 /**
7882 * Activation hook is executed after the plugin's main file is loaded, therefore,
7883 * after the plugin was loaded. The logic is located at activate_plugin()
7884 * ./wp-admin/includes/plugin.php.
7885 *
7886 * @author Vova Feldman (@svovaf)
7887 * @since 1.1.9
7888 */
7889 $this->_storage->was_plugin_loaded = true;
7890 }
7891
7892 /**
7893 * @author Leo Fajardo (@leorw)
7894 * @since 2.3.0
7895 */
7896 private function maybe_activate_addon_license() {
7897 $parent_fs = $this->get_parent_instance();
7898
7899 if (
7900 ! is_object( $parent_fs ) ||
7901 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7902 ) {
7903 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7904 return;
7905 }
7906
7907 $license = $this->get_active_parent_license();
7908 if ( ! is_object( $license ) ) {
7909 return;
7910 }
7911
7912 if (
7913 $this->is_bundle_license_auto_activation_enabled() &&
7914 ! empty( $license->products )
7915 ) {
7916 $this->activate_bundle_license( $license );
7917
7918 return;
7919 }
7920
7921 if ( ! $this->is_registered() ) {
7922 // Opt in with a license key.
7923 $this->opt_in(
7924 $parent_fs->get_current_or_network_user()->email,
7925 false,
7926 false,
7927 $license->secret_key
7928 );
7929 } else {
7930 // Activate the license.
7931 $install = $this->get_api_site_scope()->call(
7932 '/',
7933 'put',
7934 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7935 );
7936
7937 if ( ! FS_Api::is_api_error( $install ) ) {
7938 $this->_sync_addon_license( $this->get_id(), true );
7939 }
7940 }
7941 }
7942
7943 /**
7944 * @author Leo Fajardo (@leorw)
7945 * @since 2.3.0
7946 *
7947 * @param FS_Plugin_License $license
7948 */
7949 private function maybe_network_activate_addon_license( $license = null ) {
7950 $parent_fs = $this->get_parent_instance();
7951 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7952 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7953 return;
7954 }
7955
7956 $license = ( ! is_null( $license ) ) ?
7957 $license :
7958 $this->get_active_parent_license();
7959
7960 if ( ! is_object( $license ) ) {
7961 return;
7962 }
7963
7964 if (
7965 $this->is_bundle_license_auto_activation_enabled() &&
7966 ! empty( $license->products )
7967 ) {
7968 $this->activate_bundle_license( $license );
7969
7970 return;
7971 }
7972
7973 if ( ! $this->is_network_registered() ) {
7974 $sites = $this->get_sites_for_network_level_optin();
7975
7976 if ( count( $sites ) > $license->left() ) {
7977 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7978 return;
7979 }
7980
7981 // Opt in with a license key.
7982 $this->opt_in(
7983 $parent_fs->get_user()->email,
7984 false,
7985 false,
7986 $license->secret_key,
7987 false,
7988 false,
7989 false,
7990 null,
7991 $sites
7992 );
7993 } else {
7994 $blog_2_install_map = array();
7995 $site_ids = array();
7996
7997 $all_sites = Freemius::get_sites();
7998
7999 foreach ( $all_sites as $site ) {
8000 $blog_id = Freemius::get_site_blog_id( $site );
8001 $install = $this->get_install_by_blog_id( $blog_id );
8002
8003 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
8004 // Skip license activation for installs that are already associated with a license.
8005 continue;
8006 }
8007
8008 if ( is_object( $install ) ) {
8009 $blog_2_install_map[ $blog_id ] = $install;
8010 } else {
8011 $site_ids[] = $blog_id;
8012 }
8013 }
8014
8015 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
8016 return;
8017 }
8018
8019 $user = $this->get_current_or_network_user();
8020
8021 if ( ! empty( $blog_2_install_map ) ) {
8022 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
8023
8024 if ( true !== $result ) {
8025 return;
8026 }
8027 }
8028
8029 if ( ! empty( $site_ids ) ) {
8030 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
8031 }
8032 }
8033 }
8034
8035 /**
8036 * 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.
8037 *
8038 * @author Leo Fajardo (@leorw)
8039 * @since 2.4.0
8040 *
8041 * @param FS_Plugin_License $license
8042 * @param array $sites
8043 * @param int $blog_id
8044 */
8045 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8046 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8047 $license = $this->_license;
8048 }
8049
8050 if ( ! is_object( $license ) ) {
8051 return;
8052 }
8053
8054 $parent_license = ( ! empty( $license->products ) ) ?
8055 $license :
8056 $this->get_active_parent_license( $license->secret_key );
8057
8058 if ( is_object( $parent_license ) ) {
8059 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8060 }
8061 }
8062
8063 /**
8064 * Try to activate a bundle license for all the bundle products installed on the site.
8065 * (1) If a child product install already has a license, the bundle license won't be activated.
8066 * (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.
8067 * (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.
8068 *
8069 * @author Leo Fajardo (@leorw)
8070 * @since 2.4.0
8071 *
8072 * @param FS_Plugin_License $license
8073 * @param array $sites
8074 * @param int $current_blog_id
8075 */
8076 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8077 $is_network_admin = fs_is_network_admin();
8078
8079 $installs_by_blog_map = array();
8080 $site_info_by_blog_map = array();
8081
8082 /**
8083 * Try to activate the license for all supported products.
8084 *
8085 * @author Leo Fajardo
8086 */
8087 foreach ( $license->products as $product_id ) {
8088 $fs = self::get_instance_by_id( $product_id );
8089
8090 if ( ! is_object( $fs ) ) {
8091 continue;
8092 }
8093
8094 if ( ! $fs->has_paid_plan() ) {
8095 continue;
8096 }
8097
8098 if (
8099 ! $fs->is_addon() &&
8100 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8101 ) {
8102 /**
8103 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8104 * there is a context bundle.
8105 */
8106 continue;
8107 }
8108
8109 if ( $current_blog_id > 0 ) {
8110 $fs->switch_to_blog( $current_blog_id );
8111 }
8112
8113 if ( $fs->has_active_valid_license() ) {
8114 continue;
8115 }
8116
8117 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8118 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8119 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8120 continue;
8121 }
8122 } else {
8123 if ( ! $fs->is_network_active() ) {
8124 // Do not try to activate the license in the network level if the product is not network active.
8125 continue;
8126 }
8127
8128 if ( $fs->is_network_delegated_connection() ) {
8129 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8130 continue;
8131 }
8132
8133 $has_install_with_license = false;
8134
8135 // 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.
8136 $filtered_sites = array();
8137
8138 if ( empty( $sites ) ) {
8139 $all_sites = self::get_sites();
8140
8141 foreach ( $all_sites as $site ) {
8142 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8143 }
8144 } else {
8145 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8146 foreach ( $sites as $site ) {
8147 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8148 continue;
8149 }
8150
8151 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8152 }
8153 }
8154
8155 foreach ( $sites as $site ) {
8156 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8157 continue;
8158 }
8159
8160 $blog_id = $site['blog_id'];
8161
8162 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8163 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8164 }
8165
8166 $installs = $installs_by_blog_map[ $blog_id ];
8167 $install = null;
8168
8169 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8170 $install = $installs[ $fs->get_slug() ];
8171
8172 if (
8173 is_object( $install ) &&
8174 (
8175 ! FS_Site::is_valid_id( $install->id ) ||
8176 ! FS_User::is_valid_id( $install->user_id ) ||
8177 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8178 )
8179 ) {
8180 $install = null;
8181 }
8182 }
8183
8184 if (
8185 is_object( $install ) &&
8186 FS_Plugin_License::is_valid_id( $install->license_id )
8187 ) {
8188 $has_install_with_license = true;
8189 break;
8190 }
8191
8192 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8193 // Site activation delegated, don't activate bundle license on the site in the network admin.
8194 continue;
8195 }
8196
8197 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8198 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8199 }
8200
8201 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8202 }
8203
8204 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8205 // 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.
8206 continue;
8207 }
8208
8209 $sites = $filtered_sites;
8210 }
8211
8212 $fs->activate_migrated_license(
8213 $license->secret_key,
8214 null,
8215 null,
8216 $sites,
8217 ( $current_blog_id > 0 ? $current_blog_id : null )
8218 );
8219 }
8220 }
8221
8222 /**
8223 * Returns a parent license that can be activated for the context product.
8224 *
8225 * @author Leo Fajardo (@leorw)
8226 * @since 2.3.0
8227 *
8228 * @param string|null $license_key
8229 * @param bool $flush
8230 *
8231 * @return FS_Plugin_License
8232 */
8233 function get_active_parent_license( $license_key = null, $flush = true ) {
8234 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8235
8236 $fs = $this;
8237
8238 if ( $this->is_addon() ) {
8239 $parent_instance = $this->get_parent_instance();
8240
8241 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8242 $fs = $parent_instance;
8243 }
8244 }
8245
8246 $foreign_licenses = $fs->get_foreign_licenses_info(
8247 self::get_all_licenses( $this->get_parent_id() )
8248 );
8249
8250 if ( ! empty ( $foreign_licenses ) ) {
8251 $foreign_licenses = array(
8252 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8253 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8254 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8255 );
8256
8257 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8258 }
8259
8260 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8261
8262 if (
8263 ! $this->is_api_result_object( $result, 'licenses' ) ||
8264 ! is_array( $result->licenses ) ||
8265 empty( $result->licenses )
8266 ) {
8267 return null;
8268 }
8269
8270 $parent_license = null;
8271
8272 if ( empty( $license_key ) ) {
8273 $parent_license = $result->licenses[0];
8274 } else {
8275 foreach ( $result->licenses as $license ) {
8276 if ( $license_key === $license->secret_key ) {
8277 $parent_license = $license;
8278 break;
8279 }
8280 }
8281 }
8282
8283 if ( ! is_null( $parent_license ) ) {
8284 $parent_license = new FS_Plugin_License( $parent_license );
8285 }
8286
8287 return $parent_license;
8288 }
8289
8290 /**
8291 * @author Leo Fajardo (@leorw)
8292 * @since 2.3.0
8293 *
8294 * @return array
8295 */
8296 function get_sites_for_network_level_optin() {
8297 $sites = array();
8298 $all_sites = self::get_sites();
8299
8300 foreach ( $all_sites as $site ) {
8301 $blog_id = self::get_site_blog_id( $site );
8302
8303 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8304 ! $this->is_installed_on_site( $blog_id )
8305 ) {
8306 $sites[] = $this->get_site_info( $site );
8307 }
8308 }
8309
8310 return $sites;
8311 }
8312
8313 /**
8314 * Delete account.
8315 *
8316 * @author Vova Feldman (@svovaf)
8317 * @since 1.0.3
8318 *
8319 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8320 */
8321 function delete_account_event( $check_user = true ) {
8322 $this->_logger->entrance( 'slug = ' . $this->_slug );
8323
8324 if ( $check_user && ! $this->is_user_admin() ) {
8325 return;
8326 }
8327
8328 $this->do_action( 'before_account_delete' );
8329
8330 // Clear all admin notices.
8331 $this->_admin_notices->clear_all_sticky( false );
8332
8333 $this->_delete_site( false );
8334
8335 $delete_network_common_data = true;
8336
8337 if ( $this->_is_network_active ) {
8338 $installs = $this->get_blog_install_map();
8339
8340 // Don't delete common network data unless no other installs left.
8341 $delete_network_common_data = empty( $installs );
8342 }
8343
8344 if ( $delete_network_common_data ) {
8345 $this->_delete_plans( false );
8346
8347 $this->_delete_licenses( false );
8348
8349 // Delete add-ons related to plugin's account.
8350 $this->_delete_account_addons( false );
8351 }
8352
8353 // @todo Delete plans and licenses of add-ons.
8354
8355 self::$_accounts->store();
8356
8357 /**
8358 * IMPORTANT:
8359 * Clear crons must be executed before clearing all storage.
8360 * Otherwise, the cron will not be cleared.
8361 */
8362 if ( $delete_network_common_data ) {
8363 $this->clear_sync_cron();
8364 }
8365
8366 $this->clear_install_sync_cron();
8367
8368 // Clear all storage data.
8369 $this->_storage->clear_all( true, array(
8370 'is_delegated_connection',
8371 'connectivity_test',
8372 'is_on',
8373 ), false );
8374
8375 // Send delete event.
8376 $this->get_api_site_scope()->call( '/', 'delete' );
8377
8378 $this->do_action( 'after_account_delete' );
8379 }
8380
8381 /**
8382 * Delete network level account.
8383 *
8384 * @author Vova Feldman (@svovaf)
8385 * @since 2.0.0
8386 *
8387 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8388 */
8389 function delete_network_account_event( $check_user = true ) {
8390 $this->_logger->entrance( 'slug = ' . $this->_slug );
8391
8392 if ( $check_user && ! $this->is_user_admin() ) {
8393 return;
8394 }
8395
8396 $this->do_action( 'before_network_account_delete' );
8397
8398 // Clear all admin notices.
8399 $this->_admin_notices->clear_all_sticky();
8400
8401 $this->_delete_plans( false, false );
8402
8403 $this->_delete_licenses( false );
8404
8405 // Delete add-ons related to plugin's account.
8406 $this->_delete_account_addons( false );
8407
8408 // @todo Delete plans and licenses of add-ons.
8409
8410 self::$_accounts->store( true );
8411
8412 /**
8413 * IMPORTANT:
8414 * Clear crons must be executed before clearing all storage.
8415 * Otherwise, the cron will not be cleared.
8416 */
8417 $this->clear_sync_cron( true );
8418 $this->clear_install_sync_cron( true );
8419
8420 $sites = self::get_sites();
8421
8422 $install_ids = array();
8423 foreach ( $sites as $site ) {
8424 $blog_id = self::get_site_blog_id( $site );
8425
8426 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8427 continue;
8428 }
8429
8430 $install_id = $this->_delete_site( true, $blog_id );
8431
8432 // Clear all storage data.
8433 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8434
8435 if ( FS_Site::is_valid_id( $install_id ) ) {
8436 $install_ids[] = $install_id;
8437 }
8438
8439 switch_to_blog( $blog_id );
8440
8441 $this->do_action( 'after_account_delete' );
8442
8443 restore_current_blog();
8444 }
8445
8446 $this->_storage->clear_all( true, array(
8447 'connectivity_test',
8448 'is_on',
8449 ), true );
8450
8451 // Send delete event.
8452 if ( ! empty( $install_ids ) ) {
8453 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8454 }
8455
8456 $this->do_action( 'after_network_account_delete' );
8457 }
8458
8459 /**
8460 * Plugin deactivation hook.
8461 *
8462 * @author Vova Feldman (@svovaf)
8463 * @since 1.0.1
8464 */
8465 function _deactivate_plugin_hook() {
8466 $this->_logger->entrance( 'slug = ' . $this->_slug );
8467
8468 if ( ! $this->is_user_admin() ) {
8469 return;
8470 }
8471
8472 $is_network_deactivation = fs_is_network_admin();
8473 $storage_keys_for_removal = array();
8474
8475 $this->_admin_notices->clear_all_sticky();
8476
8477 $storage_keys_for_removal[] = 'sticky_optin_added';
8478 if ( isset( $this->_storage->sticky_optin_added ) ) {
8479 unset( $this->_storage->sticky_optin_added );
8480 }
8481
8482 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8483 // Remember that plugin was already installed.
8484 $this->_storage->is_plugin_new_install = false;
8485 }
8486
8487 // Hook to plugin uninstall.
8488 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8489
8490 $this->clear_module_main_file_cache();
8491 $this->clear_sync_cron( $this->_is_network_active );
8492 $this->clear_install_sync_cron();
8493
8494 if ( $this->is_registered() ) {
8495 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8496 FS_Plugin_Updater::instance( $this )->delete_update_data();
8497 }
8498
8499 if ( $is_network_deactivation ) {
8500 // Send deactivation event.
8501 $this->sync_installs( array(
8502 'is_active' => false,
8503 ) );
8504 } else {
8505 // Send deactivation event.
8506 $this->sync_install( array(
8507 'is_active' => false,
8508 ) );
8509 }
8510 } else {
8511 if ( ! $this->has_api_connectivity() ) {
8512 // Reset connectivity test cache.
8513 unset( $this->_storage->connectivity_test );
8514
8515 $storage_keys_for_removal[] = 'connectivity_test';
8516 }
8517 }
8518
8519 if ( $is_network_deactivation ) {
8520 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8521 unset( $this->_storage->sticky_optin_added_ms );
8522 }
8523
8524 if ( ! empty( $storage_keys_for_removal ) ) {
8525 $sites = self::get_sites();
8526
8527 foreach ( $sites as $site ) {
8528 $blog_id = self::get_site_blog_id( $site );
8529
8530 foreach ( $storage_keys_for_removal as $key ) {
8531 $this->_storage->remove( $key, false, $blog_id );
8532 }
8533
8534 $this->_storage->save( $blog_id );
8535 }
8536 }
8537 }
8538
8539 // Clear API cache on deactivation.
8540 FS_Api::clear_cache();
8541
8542 $this->remove_sdk_reference();
8543 }
8544
8545 /**
8546 * @author Vova Feldman (@svovaf)
8547 * @since 1.1.6
8548 */
8549 private function remove_sdk_reference() {
8550 global $fs_active_plugins;
8551
8552 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8553 if ( $this->_plugin_basename == $data->plugin_path ) {
8554 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8555 break;
8556 }
8557 }
8558
8559 fs_fallback_to_newest_active_sdk();
8560 }
8561
8562 /**
8563 * @author Vova Feldman (@svovaf)
8564 * @since 1.1.3
8565 *
8566 * @param bool $is_anonymous
8567 * @param bool|int $network_or_blog_id Since 2.0.0
8568 */
8569 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8570 // Store information regarding skip to try and opt-in the user
8571 // again in the future.
8572 $skip_info = array(
8573 'is' => $is_anonymous,
8574 'timestamp' => WP_FS__SCRIPT_START_TIME,
8575 'version' => $this->get_plugin_version(),
8576 );
8577
8578 if ( true === $network_or_blog_id ) {
8579 $this->_storage->is_anonymous_ms = $skip_info;
8580 } else {
8581 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8582 }
8583
8584 $this->network_upgrade_mode_completed();
8585
8586 // Update anonymous mode cache.
8587 $this->_is_anonymous = $is_anonymous;
8588 }
8589
8590 /**
8591 * @author Vova Feldman (@svovaf)
8592 * @since 2.0.0
8593 *
8594 * @param int $blog_id Site ID.
8595 * @param int $user_id User ID.
8596 * @param string $domain Site domain.
8597 * @param string $path Site path.
8598 * @param int $network_id Network ID. Only relevant on multi-network installations.
8599 * @param array $meta Metadata. Used to set initial site options.
8600 *
8601 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8602 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8603 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8604 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8605 */
8606 function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8607 $this->_logger->entrance();
8608
8609 if ( $this->is_premium() &&
8610 $this->is_network_connected() &&
8611 is_object( $this->_license ) &&
8612 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8613 $this->is_license_network_active( $blog_id )
8614 ) {
8615 /**
8616 * 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.
8617 */
8618 $current_blog_id = get_current_blog_id();
8619 $license = clone $this->_license;
8620
8621 $this->switch_to_blog( $blog_id );
8622
8623 // Opt-in with network user.
8624 $this->install_with_user(
8625 $this->get_network_user(),
8626 $license->secret_key,
8627 false,
8628 false,
8629 false
8630 );
8631
8632 if ( is_object( $this->_site ) ) {
8633 if ( $this->_site->license_id == $license->id ) {
8634 /**
8635 * If the license was activated successfully, sync the license data from the remote server.
8636 */
8637 $this->_license = $license;
8638 $this->sync_site_license();
8639 }
8640 }
8641
8642 $this->switch_to_blog( $current_blog_id );
8643
8644 if ( is_object( $this->_site ) ) {
8645 // Already connected (with or without a license), so no need to continue.
8646 return;
8647 }
8648 }
8649
8650 if ( $this->is_network_anonymous() ) {
8651 /**
8652 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8653 */
8654 $this->skip_site_connection( $blog_id );
8655 } else if ( $this->is_network_delegated_connection() ) {
8656 /**
8657 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8658 */
8659 $this->delegate_site_connection( $blog_id );
8660 } else if ( $this->is_network_connected() ) {
8661 /**
8662 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8663 */
8664 $current_blog_id = get_current_blog_id();
8665
8666 $this->switch_to_blog( $blog_id );
8667
8668 // Opt-in with network user.
8669 $this->install_with_user(
8670 $this->get_network_user(),
8671 false,
8672 false,
8673 false,
8674 false
8675 );
8676
8677 $this->switch_to_blog( $current_blog_id );
8678 } else {
8679 /**
8680 * If the super-admin mixed different options (connect, skip, delegated):
8681 * a) If at least one site connection was delegated, then automatically delegate connection.
8682 * 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.
8683 */
8684 $has_delegated_site = false;
8685
8686 $sites = self::get_sites();
8687 foreach ( $sites as $site ) {
8688 $blog_id = self::get_site_blog_id( $site );
8689
8690 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8691 $has_delegated_site = true;
8692 break;
8693 }
8694 }
8695
8696 if ( $has_delegated_site ) {
8697 $this->delegate_site_connection( $blog_id );
8698 } else {
8699 $this->skip_site_connection( $blog_id );
8700 }
8701 }
8702 }
8703
8704 /**
8705 * @author Vova Feldman (@svovaf)
8706 * @since 1.1.3
8707 *
8708 * @param bool|int $network_or_blog_id Since 2.0.0.
8709 */
8710 private function reset_anonymous_mode( $network_or_blog_id = 0 ) {
8711 if ( true === $network_or_blog_id ) {
8712 unset( $this->_storage->is_anonymous_ms );
8713 } else {
8714 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8715 }
8716
8717 /**
8718 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8719 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8720 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8721 *
8722 * @author Leo Fajardo (@leorw)
8723 * @since 1.2.2
8724 */
8725 if ( ! $this->_is_network_active ||
8726 0 === $network_or_blog_id ||
8727 get_current_blog_id() == $network_or_blog_id ||
8728 ( true === $network_or_blog_id && fs_is_network_admin() )
8729 ) {
8730 $this->_is_anonymous = null;
8731 }
8732 }
8733
8734 /**
8735 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8736 * deleting the account in the network level, the URL of the page to redirect to is correct.
8737 *
8738 * @author Leo Fajardo (@leorw)
8739 *
8740 * @since 2.1.3
8741 */
8742 private function maybe_set_slug_and_network_menu_exists_flag() {
8743 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8744 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8745 $this->_menu->get_slug() :
8746 $this->_slug
8747 );
8748 }
8749 }
8750
8751 /**
8752 * Clears the anonymous mode and redirects to the opt-in screen.
8753 *
8754 * @author Vova Feldman (@svovaf)
8755 * @since 1.1.7
8756 */
8757 function connect_again() {
8758 if ( ! $this->is_anonymous() ) {
8759 return;
8760 }
8761
8762 $this->reset_anonymous_mode( fs_is_network_admin() );
8763
8764 $this->maybe_set_slug_and_network_menu_exists_flag();
8765
8766 fs_redirect( $this->get_activation_url() );
8767 }
8768
8769 /**
8770 * Skip account connect, and set anonymous mode.
8771 *
8772 * @author Vova Feldman (@svovaf)
8773 * @since 1.1.1
8774 *
8775 * @param array|null $sites Since 2.0.0. Specific sites.
8776 * @param bool $skip_all_network Since 2.0.0. If true, skip connection for all sites.
8777 */
8778 function skip_connection( $sites = null, $skip_all_network = false ) {
8779 $this->_logger->entrance();
8780
8781 $this->_admin_notices->remove_sticky( 'connect_account' );
8782
8783 if ( $skip_all_network ) {
8784 $this->set_anonymous_mode( true, true );
8785 }
8786
8787 if ( ! $skip_all_network && empty( $sites ) ) {
8788 $this->skip_site_connection();
8789 } else {
8790 $uids = array();
8791
8792 if ( $skip_all_network ) {
8793 $this->set_anonymous_mode( true, true );
8794
8795 $sites = self::get_sites();
8796 foreach ( $sites as $site ) {
8797 $blog_id = self::get_site_blog_id( $site );
8798 $this->skip_site_connection( $blog_id, false );
8799 $uids[] = $this->get_anonymous_id( $blog_id );
8800 }
8801 } else if ( ! empty( $sites ) ) {
8802 foreach ( $sites as $site ) {
8803 $uids[] = $site['uid'];
8804 $this->skip_site_connection( $site['blog_id'], false );
8805 }
8806 }
8807
8808 // Send anonymous skip event.
8809 // No user identified info nor any tracking will be sent after the user skips the opt-in.
8810 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8811 'uids' => $uids,
8812 ) );
8813 }
8814
8815 $this->network_upgrade_mode_completed();
8816 }
8817
8818 /**
8819 * Skip connection for specific site in the network.
8820 *
8821 * @author Vova Feldman (@svovaf)
8822 * @since 2.0.0
8823 *
8824 * @param int|null $blog_id
8825 * @param bool $send_skip
8826 */
8827 private function skip_site_connection( $blog_id = null, $send_skip = true ) {
8828 $this->_logger->entrance();
8829
8830 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8831
8832 $this->set_anonymous_mode( true, $blog_id );
8833
8834 if ( $send_skip ) {
8835 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8836 'uids' => array( $this->get_anonymous_id( $blog_id ) ),
8837 ) );
8838 }
8839 }
8840
8841 /**
8842 * Plugin version update hook.
8843 *
8844 * @author Vova Feldman (@svovaf)
8845 * @since 1.0.4
8846 */
8847 private function update_plugin_version_event() {
8848 $this->_logger->entrance();
8849
8850 if ( ! $this->is_registered() ) {
8851 return;
8852 }
8853
8854 $this->schedule_install_sync();
8855 // $this->sync_install( array(), true );
8856 }
8857
8858 /**
8859 * Generate an MD5 signature of a plugins collection.
8860 * This helper methods used to identify changes in a plugins collection.
8861 *
8862 * @author Vova Feldman (@svovaf)
8863 * @since 2.0.0
8864 *
8865 * @param array [string]array $plugins
8866 *
8867 * @return string
8868 */
8869 private function get_plugins_thumbprint( $plugins ) {
8870 ksort( $plugins );
8871
8872 $thumbprint = '';
8873 foreach ( $plugins as $basename => $data ) {
8874 $thumbprint .= $data['slug'] . ',' .
8875 $data['Version'] . ',' .
8876 ( $data['is_active'] ? '1' : '0' ) . ';';
8877 }
8878
8879 return md5( $thumbprint );
8880 }
8881
8882 /**
8883 * Return a list of modified plugins since the last sync.
8884 *
8885 * Note:
8886 * There's no point to store a plugins counter since even if the number of
8887 * plugins didn't change, we still need to check if the versions are all the
8888 * same and the activity state is similar.
8889 *
8890 * @author Vova Feldman (@svovaf)
8891 * @since 1.1.8
8892 *
8893 * @return array|false
8894 */
8895 private function get_plugins_data_for_api() {
8896 // Alias.
8897 $site_active_plugins_option_name = 'active_plugins';
8898 $network_plugins_option_name = 'all_plugins';
8899
8900 /**
8901 * Collection of all site level active plugins.
8902 */
8903 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8904
8905 if ( ! is_object( $site_active_plugins_cache ) ) {
8906 $site_active_plugins_cache = (object) array(
8907 'timestamp' => '',
8908 'md5' => '',
8909 'plugins' => array(),
8910 );
8911 }
8912
8913 $time = time();
8914
8915 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8916 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8917 ) {
8918 // Don't send plugin updates if last update was in the past 5 min.
8919 return false;
8920 }
8921
8922 // Write timestamp to lock the logic.
8923 $site_active_plugins_cache->timestamp = $time;
8924 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8925
8926 // Reload options from DB.
8927 self::$_accounts->load( true );
8928 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8929
8930 if ( $time != $site_active_plugins_cache->timestamp ) {
8931 // If timestamp is different, then another thread captured the lock.
8932 return false;
8933 }
8934
8935 /**
8936 * Collection of all plugins (network level).
8937 */
8938 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8939
8940 if ( ! is_object( $network_plugins_cache ) ) {
8941 $network_plugins_cache = (object) array(
8942 'timestamp' => '',
8943 'md5' => '',
8944 'plugins' => array(),
8945 );
8946 }
8947
8948 // Check if there's a change in plugins.
8949 $network_plugins = self::get_network_plugins();
8950 $site_active_plugins = self::get_site_active_plugins();
8951
8952 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8953 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8954
8955 // Check if plugins status changed (version or active/inactive).
8956 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8957 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8958
8959 if ( ! $network_plugins_changed &&
8960 ! $site_active_plugins_changed
8961 ) {
8962 // No changes.
8963 return array();
8964 }
8965
8966 $plugins_update_data = array();
8967
8968 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8969 if ( ! isset( $network_plugins[ $basename ] ) ) {
8970 // Plugin uninstalled.
8971 $uninstalled_plugin_data = $data;
8972 $uninstalled_plugin_data['is_active'] = false;
8973 $uninstalled_plugin_data['is_uninstalled'] = true;
8974 $plugins_update_data[] = $uninstalled_plugin_data;
8975
8976 unset( $network_plugins[ $basename ] );
8977
8978 unset( $network_plugins_cache->plugins[ $basename ] );
8979 unset( $site_active_plugins_cache->plugins[ $basename ] );
8980
8981 continue;
8982 }
8983
8984 $was_active = $data['is_active'] ||
8985 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8986 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8987 $is_active = $network_plugins[ $basename ]['is_active'] ||
8988 ( isset( $site_active_plugins[ $basename ] ) &&
8989 $site_active_plugins[ $basename ]['is_active'] );
8990
8991 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8992 isset( $site_active_plugins[ $basename ] )
8993 ) {
8994 // Plugin was site level activated.
8995 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
8996 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8997 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8998 ! isset( $site_active_plugins[ $basename ] )
8999 ) {
9000 // Plugin was site level deactivated.
9001 unset( $site_active_plugins_cache->plugins[ $basename ] );
9002 }
9003
9004 $prev_version = $data['version'];
9005 $current_version = $network_plugins[ $basename ]['Version'];
9006
9007 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9008 // Plugin activated or deactivated, or version changed.
9009
9010 if ( $was_active !== $is_active ) {
9011 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9012 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9013 }
9014 }
9015
9016 if ( $prev_version !== $current_version ) {
9017 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9018 }
9019
9020 $updated_plugin_data = $data;
9021 $updated_plugin_data['is_active'] = $is_active;
9022 $updated_plugin_data['version'] = $current_version;
9023 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9024 $plugins_update_data[] = $updated_plugin_data;
9025 }
9026 }
9027
9028 // Find new plugins that weren't yet seen before.
9029 foreach ( $network_plugins as $basename => $data ) {
9030 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9031 // New plugin.
9032 $new_plugin = array(
9033 'slug' => $data['slug'],
9034 'version' => $data['Version'],
9035 'title' => $data['Name'],
9036 'is_active' => $data['is_active'],
9037 'is_uninstalled' => false,
9038 );
9039
9040 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9041
9042 $is_site_level_active = (
9043 isset( $site_active_plugins[ $basename ] ) &&
9044 $site_active_plugins[ $basename ]['is_active']
9045 );
9046
9047 /**
9048 * If not network active, set the activity status based on the site-level plugin status.
9049 */
9050 if ( ! $new_plugin['is_active'] ) {
9051 $new_plugin['is_active'] = $is_site_level_active;
9052 }
9053
9054 $plugins_update_data[] = $new_plugin;
9055
9056 if ( isset( $site_active_plugins[ $basename ] ) ) {
9057 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9058 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9059 }
9060 }
9061 }
9062
9063 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9064 $site_active_plugins_cache->timestamp = $time;
9065 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9066
9067 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9068 $network_plugins_cache->timestamp = $time;
9069 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9070
9071 return $plugins_update_data;
9072 }
9073
9074 /**
9075 * Return a list of modified themes since the last sync.
9076 *
9077 * Note:
9078 * There's no point to store a themes counter since even if the number of
9079 * themes didn't change, we still need to check if the versions are all the
9080 * same and the activity state is similar.
9081 *
9082 * @author Vova Feldman (@svovaf)
9083 * @since 1.1.8
9084 *
9085 * @return array|false
9086 */
9087 private function get_themes_data_for_api() {
9088 // Alias.
9089 $option_name = 'all_themes';
9090
9091 $all_cached_themes = self::$_accounts->get_option( $option_name );
9092
9093 if ( ! is_object( $all_cached_themes ) ) {
9094 $all_cached_themes = (object) array(
9095 'timestamp' => '',
9096 'md5' => '',
9097 'themes' => array(),
9098 );
9099 }
9100
9101 $time = time();
9102
9103 if ( ! empty( $all_cached_themes->timestamp ) &&
9104 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9105 ) {
9106 // Don't send theme updates if last update was in the past 5 min.
9107 return false;
9108 }
9109
9110 // Write timestamp to lock the logic.
9111 $all_cached_themes->timestamp = $time;
9112 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9113
9114 // Reload options from DB.
9115 self::$_accounts->load( true );
9116 $all_cached_themes = self::$_accounts->get_option( $option_name );
9117
9118 if ( $time != $all_cached_themes->timestamp ) {
9119 // If timestamp is different, then another thread captured the lock.
9120 return false;
9121 }
9122
9123 // Get active theme.
9124 $active_theme = wp_get_theme();
9125 $active_theme_stylesheet = $active_theme->get_stylesheet();
9126
9127 // Check if there's a change in themes.
9128 $all_themes = wp_get_themes();
9129
9130 // Check if themes changed.
9131 ksort( $all_themes );
9132
9133 $themes_signature = '';
9134 foreach ( $all_themes as $slug => $data ) {
9135 $is_active = ( $slug === $active_theme_stylesheet );
9136 $themes_signature .= $slug . ',' .
9137 $data->version . ',' .
9138 ( $is_active ? '1' : '0' ) . ';';
9139 }
9140
9141 // Check if themes status changed (version or active/inactive).
9142 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9143
9144 $themes_update_data = array();
9145
9146 if ( $themes_changed ) {
9147 // Change in themes, report changes.
9148
9149 // Update existing themes info.
9150 foreach ( $all_cached_themes->themes as $slug => $data ) {
9151 $is_active = ( $slug === $active_theme_stylesheet );
9152
9153 if ( ! isset( $all_themes[ $slug ] ) ) {
9154 // Plugin uninstalled.
9155 $uninstalled_theme_data = $data;
9156 $uninstalled_theme_data['is_active'] = false;
9157 $uninstalled_theme_data['is_uninstalled'] = true;
9158 $themes_update_data[] = $uninstalled_theme_data;
9159
9160 unset( $all_themes[ $slug ] );
9161 unset( $all_cached_themes->themes[ $slug ] );
9162 } else if ( $data['is_active'] !== $is_active ||
9163 $data['version'] !== $all_themes[ $slug ]->version
9164 ) {
9165 // Plugin activated or deactivated, or version changed.
9166
9167 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9168 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9169
9170 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9171 }
9172 }
9173
9174 // Find new themes that weren't yet seen before.
9175 foreach ( $all_themes as $slug => $data ) {
9176 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9177 $is_active = ( $slug === $active_theme_stylesheet );
9178
9179 // New plugin.
9180 $new_plugin = array(
9181 'slug' => $slug,
9182 'version' => $data->version,
9183 'title' => $data->name,
9184 'is_active' => $is_active,
9185 'is_uninstalled' => false,
9186 );
9187
9188 $themes_update_data[] = $new_plugin;
9189 $all_cached_themes->themes[ $slug ] = $new_plugin;
9190 }
9191 }
9192
9193 $all_cached_themes->md5 = md5( $themes_signature );
9194 $all_cached_themes->timestamp = time();
9195 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9196 }
9197
9198 return $themes_update_data;
9199 }
9200
9201 /**
9202 * Get site data for API install request.
9203 *
9204 * @author Vova Feldman (@svovaf)
9205 * @since 1.1.2
9206 *
9207 * @param string[] $override
9208 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9209 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9210 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, charset, title, and URL).
9211 *
9212 * @return array
9213 */
9214 private function get_install_data_for_api(
9215 array $override,
9216 $include_plugins = true,
9217 $include_themes = true,
9218 $include_blog_data = true
9219 ) {
9220 if ( $this->is_extensions_tracking_allowed() ) {
9221 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9222 /**
9223 * @since 1.1.8 Also send plugin updates.
9224 */
9225 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9226 $plugins = $this->get_plugins_data_for_api();
9227 if ( ! empty( $plugins ) ) {
9228 $override['plugins'] = $plugins;
9229 }
9230 }
9231 }
9232
9233 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9234 /**
9235 * @since 1.1.8 Also send themes updates.
9236 */
9237 if ( $include_themes && ! isset( $override['themes'] ) ) {
9238 $themes = $this->get_themes_data_for_api();
9239 if ( ! empty( $themes ) ) {
9240 $override['themes'] = $themes;
9241 }
9242 }
9243 }
9244 }
9245
9246 $versions = $this->get_versions();
9247
9248 $blog_data = $include_blog_data ?
9249 array(
9250 'language' => get_bloginfo( 'language' ),
9251 'charset' => get_bloginfo( 'charset' ),
9252 'title' => get_bloginfo( 'name' ),
9253 'url' => get_site_url(),
9254 ) :
9255 array();
9256
9257 return array_merge( $versions, $blog_data, array(
9258 'version' => $this->get_plugin_version(),
9259 'is_premium' => $this->is_premium(),
9260 // Special params.
9261 'is_active' => true,
9262 'is_disconnected' => $this->is_tracking_prohibited(),
9263 'is_uninstalled' => false,
9264 ), $override );
9265 }
9266
9267 /**
9268 * Update installs details.
9269 *
9270 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9271 *
9272 * @author Vova Feldman (@svovaf)
9273 * @since 2.0.0
9274 *
9275 * @param string[] string $override
9276 * @param bool $only_diff
9277 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9278 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9279 *
9280 * @return array
9281 */
9282 private function get_installs_data_for_api(
9283 array $override,
9284 $only_diff = false,
9285 $include_plugins = true,
9286 $include_themes = true
9287 ) {
9288 /**
9289 * @since 1.1.8 Also send plugin updates.
9290 */
9291 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9292 // $plugins = $this->get_plugins_data_for_api();
9293 // if ( ! empty( $plugins ) ) {
9294 // $override['plugins'] = $plugins;
9295 // }
9296 // }
9297 /**
9298 * @since 1.1.8 Also send themes updates.
9299 */
9300 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9301 // $themes = $this->get_themes_data_for_api();
9302 // if ( ! empty( $themes ) ) {
9303 // $override['themes'] = $themes;
9304 // }
9305 // }
9306
9307 // Common properties.
9308 $versions = $this->get_versions();
9309 $common = array_merge( $versions, array(
9310 'version' => $this->get_plugin_version(),
9311 'is_premium' => $this->is_premium(),
9312 ), $override );
9313
9314
9315 $is_common_diff_for_any_site = false;
9316 $common_diff_union = array();
9317
9318 $installs_data = array();
9319
9320 $sites = self::get_sites();
9321
9322 foreach ( $sites as $site ) {
9323 $blog_id = self::get_site_blog_id( $site );
9324
9325 $install = $this->get_install_by_blog_id( $blog_id );
9326
9327 if ( is_object( $install ) ) {
9328 if ( $install->user_id != $this->_user->id ) {
9329 // Install belongs to a different owner.
9330 continue;
9331 }
9332
9333 if ( ! $this->is_premium() && $install->is_tracking_prohibited() ) {
9334 // Don't send updates regarding opted-out installs.
9335 continue;
9336 }
9337
9338 $install_data = $this->get_site_info( $site );
9339
9340 $uid = $install_data['uid'];
9341
9342 unset( $install_data['blog_id'] );
9343 unset( $install_data['uid'] );
9344
9345 $install_data['is_disconnected'] = $install->is_disconnected;
9346 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9347 $install_data['is_uninstalled'] = $install->is_uninstalled;
9348
9349 $common_diff = null;
9350 $is_common_diff = false;
9351 if ( $only_diff ) {
9352 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9353 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9354
9355 $is_common_diff = ! empty( $common_diff );
9356
9357 if ( $is_common_diff ) {
9358 foreach ( $common_diff as $k => $v ) {
9359 if ( ! isset( $common_diff_union[ $k ] ) ) {
9360 $common_diff_union[ $k ] = $v;
9361 }
9362 }
9363 }
9364
9365 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9366 }
9367
9368 if ( ! empty( $install_data ) || $is_common_diff ) {
9369 // Add install ID and site unique ID.
9370 $install_data['id'] = $install->id;
9371 $install_data['uid'] = $uid;
9372
9373 $installs_data[] = $install_data;
9374 }
9375 }
9376 }
9377
9378 restore_current_blog();
9379
9380 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9381 if ( ! $only_diff ) {
9382 $installs_data[] = $common;
9383 } else if ( ! empty( $common_diff_union ) ) {
9384 $installs_data[] = $common_diff_union;
9385 }
9386 }
9387
9388 foreach ( $installs_data as &$data ) {
9389 $data = (object) $data;
9390 }
9391
9392 return $installs_data;
9393 }
9394
9395 /**
9396 * Compare site actual data to the stored install data and return the differences for an API data sync.
9397 *
9398 * @author Vova Feldman (@svovaf)
9399 * @since 2.0.0
9400 *
9401 * @param array $site
9402 * @param FS_Site $install
9403 * @param string[] string $override
9404 *
9405 * @return array
9406 */
9407 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9408 $diff = array();
9409 $special = array();
9410 $special_override = false;
9411
9412 foreach ( $site as $p => $v ) {
9413 if ( property_exists( $install, $p ) ) {
9414 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9415 $install->{$p} != $v
9416 ) {
9417 $install->{$p} = $v;
9418 $diff[ $p ] = $v;
9419 }
9420 } else {
9421 $special[ $p ] = $v;
9422
9423 if ( isset( $override[ $p ] ) ||
9424 'plugins' === $p ||
9425 'themes' === $p
9426 ) {
9427 $special_override = true;
9428 }
9429 }
9430 }
9431
9432 if ( $special_override || 0 < count( $diff ) ) {
9433 // Add special params only if has at least one
9434 // standard param, or if explicitly requested to
9435 // override a special param or a param which is not exist
9436 // in the install object.
9437 $diff = array_merge( $diff, $special );
9438 }
9439
9440 return $diff;
9441 }
9442
9443 /**
9444 * Update install only if changed.
9445 *
9446 * @author Vova Feldman (@svovaf)
9447 * @since 1.0.9
9448 *
9449 * @param string[] string $override
9450 * @param bool $flush
9451 *
9452 * @return false|object|string
9453 */
9454 private function send_install_update( $override = array(), $flush = false ) {
9455 $this->_logger->entrance();
9456
9457 $check_properties = $this->get_install_data_for_api( $override );
9458
9459 if ( $flush ) {
9460 $params = $check_properties;
9461 } else {
9462 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9463 }
9464
9465 $keepalive_only_update = false;
9466 if ( empty( $params ) ) {
9467 $keepalive_only_update = $this->should_send_keepalive_update();
9468
9469 if ( ! $keepalive_only_update ) {
9470 /**
9471 * There are no updates to send including keepalive.
9472 *
9473 * @author Leo Fajardo (@leorw)
9474 * @since 2.2.3
9475 */
9476 return false;
9477 }
9478 }
9479
9480 if ( ! $keepalive_only_update ) {
9481 /**
9482 * Do not update the last install sync timestamp after a keepalive-only call since there were no actual
9483 * updates sent.
9484 *
9485 * @author Leo Fajardo (@leorw)
9486 * @since 2.2.3
9487 */
9488 if ( ! is_multisite() ) {
9489 // Update last install sync timestamp.
9490 $this->set_cron_execution_timestamp( 'install_sync' );
9491 }
9492
9493 $params['uid'] = $this->get_anonymous_id();
9494 }
9495
9496 $this->set_keepalive_timestamp();
9497
9498 // Send updated values to FS.
9499 $site = $this->get_api_site_scope()->call( '/', 'put', $params );
9500
9501 if ( ! $keepalive_only_update && $this->is_api_result_entity( $site ) ) {
9502 /**
9503 * Do not clear scheduled sync after a keepalive-only call since there were no actual updates sent.
9504 *
9505 * @author Leo Fajardo (@leorw)
9506 * @since 2.2.3
9507 */
9508 if ( ! is_multisite() ) {
9509 // I successfully sent install update, clear scheduled sync if exist.
9510 $this->clear_install_sync_cron();
9511 }
9512 }
9513
9514 return $site;
9515 }
9516
9517 /**
9518 * Update installs only if changed.
9519 *
9520 * @author Vova Feldman (@svovaf)
9521 * @since 2.0.0
9522 *
9523 * @param string[] string $override
9524 * @param bool $flush
9525 *
9526 * @return false|object|string
9527 */
9528 private function send_installs_update( $override = array(), $flush = false ) {
9529 $this->_logger->entrance();
9530
9531 $installs_data = $this->get_installs_data_for_api( $override, ! $flush );
9532
9533 $keepalive_only_update = false;
9534 if ( empty( $installs_data ) ) {
9535 /**
9536 * Pass `true` to use the network level storage since the update is for many installs.
9537 *
9538 * @author Leo Fajardo (@leorw)
9539 * @since 2.2.3
9540 */
9541 $keepalive_only_update = $this->should_send_keepalive_update( true );
9542
9543 if ( ! $keepalive_only_update ) {
9544 /**
9545 * There are no updates to send including keepalive.
9546 *
9547 * @author Leo Fajardo (@leorw)
9548 * @since 2.2.3
9549 */
9550 return false;
9551 }
9552 }
9553
9554 if ( ! $keepalive_only_update ) {
9555 // Update last install sync timestamp if there were actual updates sent (i.e., not a keepalive-only call).
9556 $this->set_cron_execution_timestamp( 'install_sync' );
9557 }
9558
9559 /**
9560 * Pass `true` to use the network level storage since the update is for many installs.
9561 *
9562 * @author Leo Fajardo (@leorw)
9563 * @since 2.2.3
9564 */
9565 $this->set_keepalive_timestamp( true );
9566
9567 // Send updated values to FS.
9568 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9569
9570 if ( ! $keepalive_only_update && $this->is_api_result_object( $result, 'installs' ) ) {
9571 // I successfully sent installs update (there was an actual update sent and it's not just a keepalive-only call), clear scheduled sync if exist.
9572 $this->clear_install_sync_cron();
9573 }
9574
9575 return $result;
9576 }
9577
9578 /**
9579 * @author Leo Fajardo (@leorw)
9580 *
9581 * @param bool|null $use_network_level_storage
9582 *
9583 * @return bool
9584 */
9585 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9586 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9587
9588 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9589 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9590 return true;
9591 } else {
9592 // 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.
9593 return ( 7 == rand( 1, 7 ) );
9594 }
9595 }
9596
9597 /**
9598 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9599 *
9600 * @author Leo Fajardo (@leorw)
9601 * @since 2.3.2
9602 */
9603 private function maybe_sync_install_user() {
9604 if ( $this->_user->id == $this->_site->user_id ) {
9605 return;
9606 }
9607
9608 // Fetch user data and store if found.
9609 $this->sync_user_by_current_install();
9610 }
9611
9612 /**
9613 * Update install only if changed.
9614 *
9615 * @author Vova Feldman (@svovaf)
9616 * @since 1.0.9
9617 *
9618 * @param string[] string $override
9619 * @param bool $flush
9620 */
9621 private function sync_install( $override = array(), $flush = false ) {
9622 $this->_logger->entrance();
9623
9624 $site = $this->send_install_update( $override, $flush );
9625
9626 if ( false === $site ) {
9627 // No sync required.
9628 return;
9629 }
9630
9631 if ( ! $this->is_api_result_entity( $site ) ) {
9632 // Failed to sync, don't update locally.
9633 return;
9634 }
9635
9636 $this->_site = new FS_Site( $site );
9637
9638 $this->_store_site( true );
9639 }
9640
9641 /**
9642 * Update install only if changed.
9643 *
9644 * @author Vova Feldman (@svovaf)
9645 * @since 1.0.9
9646 *
9647 * @param string[] string $override
9648 * @param bool $flush
9649 */
9650 private function sync_installs( $override = array(), $flush = false ) {
9651 $this->_logger->entrance();
9652
9653 $result = $this->send_installs_update( $override, $flush );
9654
9655 if ( false === $result ) {
9656 // No sync required.
9657 return;
9658 }
9659
9660 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9661 // Failed to sync, don't update locally.
9662 return;
9663 }
9664
9665 $address_to_blog_map = $this->get_address_to_blog_map();
9666
9667 foreach ( $result->installs as $install ) {
9668 $this->_site = new FS_Site( $install );
9669
9670 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9671 $blog_id = $address_to_blog_map[ $address ];
9672
9673 $this->_store_site( true, $blog_id );
9674 }
9675 }
9676
9677 /**
9678 * Track install's custom event.
9679 *
9680 * IMPORTANT:
9681 * Custom event tracking is currently only supported for specific clients.
9682 * If you are not one of them, please don't use this method. If you will,
9683 * the API will simply ignore your request based on the plugin ID.
9684 *
9685 * Need custom tracking for your plugin or theme?
9686 * If you are interested in custom event tracking please contact yo@freemius.com
9687 * for further details.
9688 *
9689 * @author Vova Feldman (@svovaf)
9690 * @since 1.2.1
9691 *
9692 * @param string $name Event name.
9693 * @param array $properties Associative key/value array with primitive values only
9694 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9695 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9696 *
9697 * @return object|false Event data or FALSE on failure.
9698 *
9699 * @throws \Freemius_InvalidArgumentException
9700 */
9701 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9702 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9703
9704 if ( ! $this->is_registered() ) {
9705 return false;
9706 }
9707
9708 $event = array( 'type' => $name );
9709
9710 if ( is_numeric( $process_at ) && $process_at > time() ) {
9711 $event['process_at'] = $process_at;
9712 }
9713
9714 if ( $once ) {
9715 $event['once'] = true;
9716 }
9717
9718 if ( ! empty( $properties ) ) {
9719 // Verify associative array values are primitive.
9720 foreach ( $properties as $k => $v ) {
9721 if ( ! is_scalar( $v ) ) {
9722 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9723 }
9724 }
9725
9726 $event['properties'] = $properties;
9727 }
9728
9729 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9730
9731 return $this->is_api_error( $result ) ?
9732 false :
9733 $result;
9734 }
9735
9736 /**
9737 * Track install's custom event only once, but it still triggers the API call.
9738 *
9739 * IMPORTANT:
9740 * Custom event tracking is currently only supported for specific clients.
9741 * If you are not one of them, please don't use this method. If you will,
9742 * the API will simply ignore your request based on the plugin ID.
9743 *
9744 * Need custom tracking for your plugin or theme?
9745 * If you are interested in custom event tracking please contact yo@freemius.com
9746 * for further details.
9747 *
9748 * @author Vova Feldman (@svovaf)
9749 * @since 1.2.1
9750 *
9751 * @param string $name Event name.
9752 * @param array $properties Associative key/value array with primitive values only
9753 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9754 *
9755 * @return object|false Event data or FALSE on failure.
9756 *
9757 * @throws \Freemius_InvalidArgumentException
9758 *
9759 * @user Freemius::track_event()
9760 */
9761 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9762 return $this->track_event( $name, $properties, $process_at, true );
9763 }
9764
9765 /**
9766 * Plugin uninstall hook.
9767 *
9768 * @author Vova Feldman (@svovaf)
9769 * @since 1.0.1
9770 *
9771 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9772 */
9773 function _uninstall_plugin_event( $check_user = true ) {
9774 $this->_logger->entrance( 'slug = ' . $this->_slug );
9775
9776 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9777 return;
9778 }
9779
9780 $params = array();
9781 $uninstall_reason = null;
9782 if ( isset( $this->_storage->uninstall_reason ) ) {
9783 $uninstall_reason = $this->_storage->uninstall_reason;
9784 $params['reason_id'] = $uninstall_reason->id;
9785 $params['reason_info'] = $uninstall_reason->info;
9786 }
9787
9788 if ( ! $this->is_registered() ) {
9789 // Send anonymous uninstall event only if user submitted a feedback.
9790 if ( isset( $uninstall_reason ) ) {
9791 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9792 $this->opt_in( false, false, false, false, true );
9793 } else {
9794 $params['uid'] = $this->get_anonymous_id();
9795 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9796 }
9797 }
9798 } else {
9799 $params = array_merge( $params, array(
9800 'is_active' => false,
9801 'is_uninstalled' => true,
9802 ) );
9803
9804 if ( $this->_is_network_active ) {
9805 // Send uninstall event.
9806 $this->send_installs_update( $params );
9807 } else {
9808 // Send uninstall event.
9809 $this->send_install_update( $params );
9810 }
9811 }
9812
9813 // @todo Decide if we want to delete plugin information from db.
9814 }
9815
9816 /**
9817 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9818 *
9819 * @author Vova Feldman (@svovaf)
9820 * @since 2.2.1
9821 *
9822 * @param string $is_premium
9823 * @param string $caller
9824 *
9825 * @return string
9826 */
9827 function set_basename( $is_premium, $caller ) {
9828 $basename = plugin_basename( $caller );
9829
9830 $current_basename = $is_premium ?
9831 $this->_premium_plugin_basename :
9832 $this->_free_plugin_basename;
9833
9834 if ( $current_basename == $basename ) {
9835 // Basename value set correctly.
9836 return;
9837 }
9838
9839 if ( $is_premium ) {
9840 $this->_premium_plugin_basename = $basename;
9841 } else {
9842 $this->_free_plugin_basename = $basename;
9843 }
9844
9845 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9846
9847 register_activation_hook(
9848 $plugin_dir . $basename,
9849 array( &$this, '_activate_plugin_event_hook' )
9850 );
9851 }
9852
9853 /**
9854 * @author Vova Feldman (@svovaf)
9855 * @since 1.1.1
9856 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9857 *
9858 * @return string
9859 */
9860 function premium_plugin_basename() {
9861 if ( ! isset( $this->_premium_plugin_basename ) ) {
9862 $this->_premium_plugin_basename = $this->is_premium() ?
9863 // The product is premium, so use the current basename.
9864 $this->_plugin_basename :
9865 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9866 }
9867
9868 return $this->_premium_plugin_basename;
9869 }
9870
9871 /**
9872 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9873 *
9874 * @author Vova Feldman (@svovaf)
9875 * @since 1.0.2
9876 */
9877 public static function _uninstall_plugin_hook() {
9878 self::_load_required_static();
9879
9880 self::$_static_logger->entrance();
9881
9882 if ( ! current_user_can( 'activate_plugins' ) ) {
9883 return;
9884 }
9885
9886 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9887
9888 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9889
9890 define( 'WP_FS__UNINSTALL_MODE', true );
9891
9892 $fs = self::get_instance_by_file( $plugin_file );
9893
9894 if ( is_object( $fs ) ) {
9895 $fs->remove_sdk_reference();
9896
9897 self::require_plugin_essentials();
9898
9899 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9900 is_plugin_active( $fs->premium_plugin_basename() )
9901 ) {
9902 // Deleting Free or Premium plugin version while the other version still installed.
9903 return;
9904 }
9905
9906 $fs->_uninstall_plugin_event();
9907
9908 $fs->do_action( 'after_uninstall' );
9909 }
9910 }
9911
9912 #----------------------------------------------------------------------------------
9913 #region Plugin Information
9914 #----------------------------------------------------------------------------------
9915
9916 /**
9917 * Load WordPress core plugin.php essential module.
9918 *
9919 * @author Vova Feldman (@svovaf)
9920 * @since 1.1.1
9921 */
9922 private static function require_plugin_essentials() {
9923 if ( ! function_exists( 'get_plugins' ) ) {
9924 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9925
9926 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9927 }
9928 }
9929
9930 /**
9931 * Load WordPress core pluggable.php module.
9932 *
9933 * @author Vova Feldman (@svovaf)
9934 * @since 1.1.2
9935 */
9936 private static function require_pluggable_essentials() {
9937 if ( ! function_exists( 'wp_get_current_user' ) ) {
9938 require_once ABSPATH . 'wp-includes/pluggable.php';
9939 }
9940 }
9941
9942 /**
9943 * Return plugin data.
9944 *
9945 * @author Vova Feldman (@svovaf)
9946 * @since 1.0.1
9947 *
9948 * @param bool $reparse_plugin_metadata
9949 *
9950 * @return array
9951 */
9952 function get_plugin_data( $reparse_plugin_metadata = false ) {
9953 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9954 self::require_plugin_essentials();
9955
9956 if ( $this->is_plugin() ) {
9957 /**
9958 * @author Vova Feldman (@svovaf)
9959 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9960 *
9961 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9962 */
9963 $plugin_data = get_plugin_data(
9964 $this->_plugin_main_file_path,
9965 false,
9966 false
9967 );
9968 } else {
9969 $theme_data = wp_get_theme();
9970
9971 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9972 $parent_theme = $theme_data->parent();
9973
9974 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9975 $theme_data = $parent_theme;
9976 }
9977 }
9978
9979 $plugin_data = array(
9980 'Name' => $theme_data->get( 'Name' ),
9981 'Version' => $theme_data->get( 'Version' ),
9982 'Author' => $theme_data->get( 'Author' ),
9983 'Description' => $theme_data->get( 'Description' ),
9984 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9985 );
9986 }
9987
9988 $this->_plugin_data = $plugin_data;
9989 }
9990
9991 return $this->_plugin_data;
9992 }
9993
9994 /**
9995 * @author Vova Feldman (@svovaf)
9996 * @since 1.0.1
9997 * @since 1.2.2.5 If slug not set load slug by module ID.
9998 *
9999 * @return string Plugin slug.
10000 */
10001 function get_slug() {
10002 if ( ! isset( $this->_slug ) ) {
10003 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10004 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10005 }
10006
10007 return $this->_slug;
10008 }
10009
10010 /**
10011 * @author Leo Fajardo (@leorw)
10012 * @since 2.2.1
10013 *
10014 * @return string
10015 */
10016 function get_premium_slug() {
10017 return is_object( $this->_plugin ) ?
10018 $this->_plugin->premium_slug :
10019 "{$this->_slug}-premium";
10020 }
10021
10022 /**
10023 * Retrieve the desired folder name for the product.
10024 *
10025 * @author Vova Feldman (@svovaf)
10026 * @since 1.2.1.7
10027 *
10028 * @return string Plugin slug.
10029 */
10030 function get_target_folder_name() {
10031 return $this->can_use_premium_code() ?
10032 $this->_plugin->premium_slug :
10033 $this->_slug;
10034 }
10035
10036 /**
10037 * @author Vova Feldman (@svovaf)
10038 * @since 1.0.1
10039 *
10040 * @return number Plugin ID.
10041 */
10042 function get_id() {
10043 return $this->_plugin->id;
10044 }
10045
10046 /**
10047 * @author Leo Fajardo (@leorw)
10048 * @since 2.2.4
10049 *
10050 * @return number|null Bundle ID.
10051 */
10052 function get_bundle_id() {
10053 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10054 $this->_plugin->bundle_id :
10055 null;
10056 }
10057
10058 /**
10059 * @author Vova Feldman (@svovaf)
10060 * @since 2.3.1
10061 *
10062 * @return string|null Bundle public key.
10063 */
10064 function get_bundle_public_key() {
10065 return isset( $this->_plugin->bundle_public_key ) ?
10066 $this->_plugin->bundle_public_key :
10067 null;
10068 }
10069
10070 /**
10071 * @author Vova Feldman (@svovaf)
10072 * @since 1.2.1.5
10073 *
10074 * @return string Freemius SDK version
10075 */
10076 function get_sdk_version() {
10077 return $this->version;
10078 }
10079
10080 /**
10081 * @author Vova Feldman (@svovaf)
10082 * @since 1.2.1.5
10083 *
10084 * @return number Parent plugin ID (if parent exist).
10085 */
10086 function get_parent_id() {
10087 return $this->is_addon() ?
10088 $this->get_parent_instance()->get_id() :
10089 $this->_plugin->id;
10090 }
10091
10092 /**
10093 * @author Vova Feldman (@svovaf)
10094 * @since 2.3.1
10095 *
10096 * @return string
10097 */
10098 function get_usage_tracking_terms_url() {
10099 return $this->apply_filters(
10100 'usage_tracking_terms_url',
10101 "https://freemius.com/wordpress/usage-tracking/{$this->_plugin->id}/{$this->_slug}/"
10102 );
10103 }
10104
10105 /**
10106 * @author Vova Feldman (@svovaf)
10107 * @since 2.3.1
10108 *
10109 * @return string
10110 */
10111 function get_eula_url() {
10112 return $this->apply_filters(
10113 'eula_url',
10114 "https://freemius.com/terms/{$this->_plugin->id}/{$this->_slug}/"
10115 );
10116 }
10117
10118 /**
10119 * @author Vova Feldman (@svovaf)
10120 * @since 1.0.1
10121 *
10122 * @return string Plugin public key.
10123 */
10124 function get_public_key() {
10125 return $this->_plugin->public_key;
10126 }
10127
10128 /**
10129 * Will be available only on sandbox mode.
10130 *
10131 * @author Vova Feldman (@svovaf)
10132 * @since 1.0.4
10133 *
10134 * @return mixed Plugin secret key.
10135 */
10136 function get_secret_key() {
10137 return $this->_plugin->secret_key;
10138 }
10139
10140 /**
10141 * @author Vova Feldman (@svovaf)
10142 * @since 1.1.1
10143 *
10144 * @return bool
10145 */
10146 function has_secret_key() {
10147 return ! empty( $this->_plugin->secret_key );
10148 }
10149
10150 /**
10151 * @author Vova Feldman (@svovaf)
10152 * @since 1.0.9
10153 *
10154 * @param string|bool $premium_suffix
10155 *
10156 * @return string
10157 */
10158 function get_plugin_name( $premium_suffix = false ) {
10159 $this->_logger->entrance();
10160
10161 /**
10162 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10163 *
10164 * @author Vova Feldman
10165 */
10166 if ( ! isset( $this->_plugin_name ) ) {
10167 // Name is not yet set.
10168 $this->set_name( $premium_suffix );
10169 } else if (
10170 ! empty( $premium_suffix ) &&
10171 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10172 ) {
10173 // Name is already set, but there's a change in the premium suffix.
10174 $this->set_name( $premium_suffix );
10175 }
10176
10177 return $this->_plugin_name;
10178 }
10179
10180 /**
10181 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10182 *
10183 * @author Vova Feldman (@svovaf)
10184 * @since 2.2.1
10185 *
10186 * @param string $premium_suffix
10187 */
10188 private function set_name( $premium_suffix = '' ) {
10189 $plugin_data = $this->get_plugin_data();
10190
10191 // Get name.
10192 $this->_plugin_name = $plugin_data['Name'];
10193
10194 if ( is_string( $premium_suffix ) ) {
10195 $premium_suffix = trim( $premium_suffix );
10196
10197 if ( ! empty( $premium_suffix ) ) {
10198 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10199 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10200 $suffix_len = strlen( $suffix );
10201
10202 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10203 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10204 ) {
10205 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10206 }
10207 }
10208 }
10209
10210 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10211 }
10212
10213 /**
10214 * @author Vova Feldman (@svovaf)
10215 * @since 1.0.0
10216 *
10217 * @param bool $reparse_plugin_metadata
10218 *
10219 * @return string
10220 */
10221 function get_plugin_version( $reparse_plugin_metadata = false ) {
10222 $this->_logger->entrance();
10223
10224 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10225
10226 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10227
10228 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10229 }
10230
10231 /**
10232 * @author Vova Feldman (@svovaf)
10233 * @since 1.2.1.7
10234 *
10235 * @return string
10236 */
10237 function get_plugin_title() {
10238 $this->_logger->entrance();
10239
10240 $title = $this->_plugin->title;
10241
10242 return $this->apply_filters( 'plugin_title', $title );
10243 }
10244
10245 /**
10246 * @author Vova Feldman (@svovaf)
10247 * @since 1.2.2.7
10248 *
10249 * @param bool $lowercase
10250 *
10251 * @return string
10252 */
10253 function get_module_label( $lowercase = false ) {
10254 $label = $this->is_addon() ?
10255 $this->get_text_inline( 'Add-On', 'addon' ) :
10256 ( $this->is_plugin() ?
10257 $this->get_text_inline( 'Plugin', 'plugin' ) :
10258 $this->get_text_inline( 'Theme', 'theme' ) );
10259
10260 if ( $lowercase ) {
10261 $label = strtolower( $label );
10262 }
10263
10264 return $label;
10265 }
10266
10267 /**
10268 * @author Vova Feldman (@svovaf)
10269 * @since 1.0.4
10270 *
10271 * @return string
10272 */
10273 function get_plugin_basename() {
10274 if ( ! isset( $this->_plugin_basename ) ) {
10275 if ( $this->is_plugin() ) {
10276 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10277 } else {
10278 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10279 }
10280 }
10281
10282 return $this->_plugin_basename;
10283 }
10284
10285 function get_plugin_folder_name() {
10286 $this->_logger->entrance();
10287
10288 $plugin_folder = $this->_plugin_basename;
10289
10290 while ( '.' !== dirname( $plugin_folder ) ) {
10291 $plugin_folder = dirname( $plugin_folder );
10292 }
10293
10294 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10295
10296 return $plugin_folder;
10297 }
10298
10299 #endregion ------------------------------------------------------------------
10300
10301 /* Account
10302 ------------------------------------------------------------------------------------------------------------------*/
10303
10304 /**
10305 * Find plugin's slug by plugin's basename.
10306 *
10307 * @author Vova Feldman (@svovaf)
10308 * @since 1.0.9
10309 *
10310 * @param string $plugin_base_name
10311 *
10312 * @return false|string
10313 */
10314 private static function find_slug_by_basename( $plugin_base_name ) {
10315 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10316
10317 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10318 return false;
10319 }
10320
10321 return $file_slug_map[ $plugin_base_name ];
10322 }
10323
10324 /**
10325 * Store the map between the plugin's basename to the slug.
10326 *
10327 * @author Vova Feldman (@svovaf)
10328 * @since 1.0.9
10329 */
10330 private function store_file_slug_map() {
10331 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10332
10333 if ( ! array( $file_slug_map ) ) {
10334 $file_slug_map = array();
10335 }
10336
10337 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10338 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10339 ) {
10340 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10341 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10342 }
10343 }
10344
10345 /**
10346 * @return array[number]FS_User
10347 */
10348 static function get_all_users() {
10349 $users = self::maybe_get_entities_account_option( 'users', array() );
10350
10351 if ( ! is_array( $users ) ) {
10352 $users = array();
10353 }
10354
10355 return $users;
10356 }
10357
10358 /**
10359 * @param string $module_type
10360 * @param null|int $blog_id Since 2.0.0
10361 *
10362 * @return array[string]FS_Site
10363 */
10364 private static function get_all_sites(
10365 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10366 $blog_id = null
10367 ) {
10368 $sites = self::get_account_option( 'sites', $module_type, $blog_id );
10369
10370 if ( ! is_array( $sites ) ) {
10371 $sites = array();
10372 }
10373
10374 return $sites;
10375 }
10376
10377 /**
10378 * @author Leo Fajardo (@leorw)
10379 *
10380 * @since 1.2.2
10381 *
10382 * @param string $option_name
10383 * @param string $module_type
10384 * @param null|int $network_level_or_blog_id Since 2.0.0
10385 *
10386 * @return mixed
10387 */
10388 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10389 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10390 $option_name = $module_type . '_' . $option_name;
10391 }
10392
10393 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10394 }
10395
10396 /**
10397 * @author Leo Fajardo (@leorw)
10398 *
10399 * @since 1.2.2
10400 *
10401 * @param string $option_name
10402 * @param mixed $option_value
10403 * @param bool $store
10404 * @param null|int $network_level_or_blog_id Since 2.0.0
10405 */
10406 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10407 self::set_account_option_by_module(
10408 $this->_module_type,
10409 $option_name,
10410 $option_value,
10411 $store,
10412 $network_level_or_blog_id
10413 );
10414 }
10415
10416 /**
10417 * @author Vova Feldman (@svovaf)
10418 *
10419 * @since 1.2.2.7
10420 *
10421 * @param string $module_type
10422 * @param string $option_name
10423 * @param mixed $option_value
10424 * @param bool $store
10425 * @param null|int $network_level_or_blog_id Since 2.0.0
10426 */
10427 private static function set_account_option_by_module(
10428 $module_type,
10429 $option_name,
10430 $option_value,
10431 $store,
10432 $network_level_or_blog_id = null
10433 ) {
10434 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10435 $option_name = $module_type . '_' . $option_name;
10436 }
10437
10438 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10439 }
10440
10441 /**
10442 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10443 *
10444 * @author Leo Fajardo (@leorw)
10445 * @since 2.3.1
10446 *
10447 * @param string $option_name
10448 * @param mixed $default
10449 * @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).
10450 *
10451 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10452 */
10453 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10454 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10455
10456 $class_name = '';
10457
10458 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10459 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10460 }
10461
10462 switch ( $option_name ) {
10463 case 'plugins':
10464 case 'themes':
10465 case 'addons':
10466 $class_name = FS_Plugin::get_class_name();
10467 break;
10468 case 'users':
10469 $class_name = FS_User::get_class_name();
10470 break;
10471 case 'sites':
10472 $class_name = FS_Site::get_class_name();
10473 break;
10474 case 'licenses':
10475 case 'all_licenses':
10476 $class_name = FS_Plugin_License::get_class_name();
10477 break;
10478 case 'plans':
10479 $class_name = FS_Plugin_Plan::get_class_name();
10480 break;
10481 case 'updates':
10482 $class_name = FS_Plugin_Tag::get_class_name();
10483 break;
10484 }
10485
10486 if ( empty( $class_name ) ) {
10487 return $option;
10488 }
10489
10490 return fs_get_entities( $option, $class_name );
10491 }
10492
10493 /**
10494 * @author Vova Feldman (@svovaf)
10495 * @since 1.0.6
10496 *
10497 * @param number|null $module_id
10498 *
10499 * @return FS_Plugin_License[]
10500 */
10501 private static function get_all_licenses( $module_id = null ) {
10502 $licenses = self::get_account_option( 'all_licenses' );
10503
10504 if ( ! is_array( $licenses ) ) {
10505 $licenses = array();
10506 }
10507
10508 if ( is_null( $module_id ) ) {
10509 return $licenses;
10510 }
10511
10512 $licenses = isset( $licenses[ $module_id ] ) ?
10513 $licenses[ $module_id ] :
10514 array();
10515
10516 return $licenses;
10517 }
10518
10519 /**
10520 * @author Leo Fajardo (@leorw)
10521 * @since 2.0.0
10522 *
10523 * @return array
10524 */
10525 private static function get_all_licenses_by_module_type() {
10526 $licenses = self::get_account_option( 'all_licenses' );
10527
10528 $licenses_by_module_type = array(
10529 WP_FS__MODULE_TYPE_PLUGIN => array(),
10530 WP_FS__MODULE_TYPE_THEME => array()
10531 );
10532
10533 if ( ! is_array( $licenses ) ) {
10534 return $licenses_by_module_type;
10535 }
10536
10537 foreach ( $licenses as $module_id => $module_licenses ) {
10538 $fs = self::get_instance_by_id( $module_id );
10539 if ( false === $fs ) {
10540 continue;
10541 }
10542
10543 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10544 }
10545
10546 return $licenses_by_module_type;
10547 }
10548
10549 /**
10550 * @author Leo Fajardo (@leorw)
10551 * @since 2.0.0
10552 *
10553 * @param number $module_id
10554 * @param number|null $user_id
10555 *
10556 * @return array
10557 */
10558 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10559 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10560
10561 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10562 $all_modules_user_id_license_ids_map = array();
10563 }
10564
10565 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10566 $all_modules_user_id_license_ids_map[ $module_id ] :
10567 array();
10568
10569 if ( FS_User::is_valid_id( $user_id ) ) {
10570 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10571 $user_id_license_ids_map[ $user_id ] :
10572 array();
10573 }
10574
10575 return $user_id_license_ids_map;
10576 }
10577
10578 /**
10579 * @author Leo Fajardo (@leorw)
10580 * @since 2.0.0
10581 *
10582 * @param array $new_user_id_license_ids_map
10583 * @param number $module_id
10584 * @param number|null $user_id
10585 */
10586 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10587 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10588 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10589 $all_modules_user_id_license_ids_map = array();
10590 }
10591
10592 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10593 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10594 }
10595
10596 if ( FS_User::is_valid_id( $user_id ) ) {
10597 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10598 } else {
10599 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10600 }
10601
10602 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10603 }
10604
10605 /**
10606 * Get a collection of the user's linked license IDs.
10607 *
10608 * @author Vova Feldman (@svovaf)
10609 * @since 2.0.0
10610 *
10611 * @param number $user_id
10612 *
10613 * @return number[]
10614 */
10615 private function get_user_linked_license_ids( $user_id ) {
10616 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10617 }
10618
10619 /**
10620 * Override the user's linked license IDs with a new IDs collection.
10621 *
10622 * @author Vova Feldman (@svovaf)
10623 * @since 2.0.0
10624 *
10625 * @param number $user_id
10626 * @param number[] $license_ids
10627 */
10628 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10629 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10630 }
10631
10632 /**
10633 * Link a specified license ID to a given user.
10634 *
10635 * @author Vova Feldman (@svovaf)
10636 * @since 2.0.0
10637 *
10638 * @param number $license_id
10639 * @param number $user_id
10640 */
10641 private function link_license_2_user( $license_id, $user_id ) {
10642 $license_ids = $this->get_user_linked_license_ids( $user_id );
10643
10644 if ( in_array( $license_id, $license_ids ) ) {
10645 // License already linked.
10646 return;
10647 }
10648
10649 $license_ids[] = $license_id;
10650
10651 $this->set_user_linked_license_ids( $user_id, $license_ids );
10652 }
10653
10654 /**
10655 * @param string|bool $module_type
10656 *
10657 * @return FS_Plugin_Plan[]
10658 */
10659 private static function get_all_plans( $module_type = false ) {
10660 $plans = self::get_account_option( 'plans', $module_type );
10661
10662 if ( ! is_array( $plans ) ) {
10663 $plans = array();
10664 }
10665
10666 return $plans;
10667 }
10668
10669 /**
10670 * @author Vova Feldman (@svovaf)
10671 * @since 1.0.4
10672 *
10673 * @return FS_Plugin_Tag[]
10674 */
10675 private static function get_all_updates() {
10676 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10677
10678 if ( ! is_array( $updates ) ) {
10679 $updates = array();
10680 }
10681
10682 return $updates;
10683 }
10684
10685 /**
10686 * @author Vova Feldman (@svovaf)
10687 * @since 1.0.6
10688 *
10689 * @return array<number,FS_Plugin[]>|false
10690 */
10691 private static function get_all_addons() {
10692 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10693
10694 if ( ! is_array( $addons ) ) {
10695 $addons = array();
10696 }
10697
10698 return $addons;
10699 }
10700
10701 /**
10702 * @author Vova Feldman (@svovaf)
10703 * @since 1.0.6
10704 *
10705 * @return number[]|false
10706 */
10707 private static function get_all_account_addons() {
10708 $addons = self::$_accounts->get_option( 'account_addons', array() );
10709
10710 if ( ! is_array( $addons ) ) {
10711 $addons = array();
10712 }
10713
10714 return $addons;
10715 }
10716
10717 /**
10718 * Check if user has connected his account (opted-in).
10719 *
10720 * Note:
10721 * If the user opted-in and opted-out on a later stage,
10722 * this will still return true. If you want to check if the
10723 * user is currently opted-in, use:
10724 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10725 *
10726 * @author Vova Feldman (@svovaf)
10727 * @since 1.0.1
10728 * @return bool
10729 */
10730 function is_registered() {
10731 return is_object( $this->_user );
10732 }
10733
10734 /**
10735 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10736 *
10737 * @author Leo Fajardo (@leorw)
10738 * @since 1.2.1.5
10739 *
10740 * @return bool
10741 */
10742 function is_tracking_allowed() {
10743 return ( is_object( $this->_site ) && $this->_site->is_tracking_allowed() );
10744 }
10745
10746 /**
10747 * @author Leo Fajardo (@leorw)
10748 * @since 2.4.0
10749 *
10750 * @return bool
10751 */
10752 function is_bundle_license_auto_activation_enabled() {
10753 return $this->is_addon() ?
10754 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10755 $this->_is_bundle_license_auto_activation_enabled;
10756 }
10757
10758 /**
10759 * @author Vova Feldman (@svovaf)
10760 * @since 1.0.4
10761 *
10762 * @return FS_Plugin
10763 */
10764 function get_plugin() {
10765 return $this->_plugin;
10766 }
10767
10768 /**
10769 * @author Vova Feldman (@svovaf)
10770 * @since 1.0.3
10771 *
10772 * @return FS_User
10773 */
10774 function get_user() {
10775 return $this->_user;
10776 }
10777
10778 /**
10779 * @author Vova Feldman (@svovaf)
10780 * @since 1.0.3
10781 *
10782 * @return FS_Site
10783 */
10784 function get_site() {
10785 return $this->_site;
10786 }
10787
10788 /**
10789 * Get plugin add-ons.
10790 *
10791 * @author Vova Feldman (@svovaf)
10792 * @since 1.0.6
10793 *
10794 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10795 *
10796 * @param bool $flush
10797 *
10798 * @return FS_Plugin[]|false
10799 */
10800 function get_addons( $flush = false ) {
10801 $this->_logger->entrance();
10802
10803 if ( ! $this->_has_addons ) {
10804 return false;
10805 }
10806
10807 $addons = $this->sync_addons( $flush );
10808
10809 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10810 false :
10811 $addons;
10812 }
10813
10814 /**
10815 * @author Vova Feldman (@svovaf)
10816 * @since 1.0.6
10817 *
10818 * @return number[]|false
10819 */
10820 function get_account_addons() {
10821 $this->_logger->entrance();
10822
10823 $addons = self::get_all_account_addons();
10824
10825 if ( ! is_array( $addons ) ||
10826 ! isset( $addons[ $this->_plugin->id ] ) ||
10827 ! is_array( $addons[ $this->_plugin->id ] ) ||
10828 0 === count( $addons[ $this->_plugin->id ] )
10829 ) {
10830 return false;
10831 }
10832
10833 return $addons[ $this->_plugin->id ];
10834 }
10835
10836 /**
10837 * Check if user has any
10838 *
10839 * @author Vova Feldman (@svovaf)
10840 * @since 1.1.6
10841 *
10842 * @return bool
10843 */
10844 function has_account_addons() {
10845 $addons = $this->get_account_addons();
10846
10847 return is_array( $addons ) && ( 0 < count( $addons ) );
10848 }
10849
10850
10851 /**
10852 * Get add-on by ID (from local data).
10853 *
10854 * @author Vova Feldman (@svovaf)
10855 * @since 1.0.6
10856 *
10857 * @param number $id
10858 *
10859 * @return FS_Plugin|false
10860 */
10861 function get_addon( $id ) {
10862 $this->_logger->entrance();
10863
10864 $addons = $this->get_addons();
10865
10866 if ( is_array( $addons ) ) {
10867 foreach ( $addons as $addon ) {
10868 if ( $id == $addon->id ) {
10869 return $addon;
10870 }
10871 }
10872 }
10873
10874 return false;
10875 }
10876
10877 /**
10878 * Get add-on by slug (from local data).
10879 *
10880 * @author Vova Feldman (@svovaf)
10881 * @since 1.0.6
10882 *
10883 * @param string $slug
10884 *
10885 * @param bool $flush
10886 *
10887 * @return FS_Plugin|false
10888 */
10889 function get_addon_by_slug( $slug, $flush = false ) {
10890 $this->_logger->entrance();
10891
10892 $addons = $this->get_addons( $flush );
10893
10894 if ( is_array( $addons ) ) {
10895 foreach ( $addons as $addon ) {
10896 if ( $slug === $addon->slug ) {
10897 return $addon;
10898 }
10899 }
10900 }
10901
10902 return false;
10903 }
10904
10905 /**
10906 * @var array<number,object[]> {
10907 * @key number Add-on ID.
10908 * @val object[] The add-on's plans and prices object.
10909 * }
10910 */
10911 private $plans_and_pricing_by_addon_id;
10912
10913 /**
10914 * @author Leo Fajardo (@leorw)
10915 * @since 2.3.0
10916 *
10917 * @return array<number,object[]> {
10918 * @key number Add-on ID.
10919 * @val object[] The add-on's plans and prices object.
10920 * }
10921 */
10922 function _get_addons_plans_and_pricing_map_by_id() {
10923 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
10924 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
10925
10926 $plans_and_pricing_by_addon_id = array();
10927 if ( $this->is_api_result_object( $result, 'addons' ) ) {
10928 foreach ( $result->addons as $addon ) {
10929 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
10930 }
10931 }
10932
10933 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
10934 }
10935
10936 return $this->plans_and_pricing_by_addon_id;
10937 }
10938
10939 /**
10940 * @author Leo Fajardo (@leorw)
10941 * @since 2.3.0
10942 *
10943 * @param number $addon_id
10944 * @param bool $is_installed
10945 *
10946 * @return array
10947 */
10948 function _get_addon_info( $addon_id, $is_installed ) {
10949 $addon = $this->get_addon( $addon_id );
10950
10951 if ( ! is_object( $addon ) ) {
10952 // Unexpected call.
10953 return array();
10954 }
10955
10956 $slug = $addon->slug;
10957
10958 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
10959
10960 if ( ! fs_is_network_admin() ) {
10961 // Get blog-level activated installations.
10962 $sites = self::maybe_get_entities_account_option( 'sites', array() );
10963 } else {
10964 $sites = null;
10965
10966 if ( $this->is_addon_activated( $addon_id ) &&
10967 $this->get_addon_instance( $addon_id )->is_network_active()
10968 ) {
10969 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
10970 // Get network-level activated installations.
10971 $sites = self::maybe_get_entities_account_option(
10972 'sites',
10973 array(),
10974 $addon_storage->network_install_blog_id
10975 );
10976 }
10977 }
10978 }
10979
10980 $addon_info = array(
10981 'is_connected' => false,
10982 'slug' => $slug,
10983 'title' => $addon->title,
10984 'is_whitelabeled' => $addon_storage->is_whitelabeled
10985 );
10986
10987 if ( ! $is_installed ) {
10988 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
10989
10990 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
10991 $has_paid_plan = false;
10992 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
10993
10994 if ( is_array( $plans ) && count( $plans ) > 0 ) {
10995 foreach ( $plans as $plan ) {
10996 if ( isset( $plan->pricing ) &&
10997 is_array( $plan->pricing ) &&
10998 count( $plan->pricing ) > 0
10999 ) {
11000 $has_paid_plan = true;
11001 break;
11002 }
11003 }
11004 }
11005
11006 $addon_info['has_paid_plan'] = $has_paid_plan;
11007 }
11008 }
11009
11010 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11011 return $addon_info;
11012 }
11013
11014 $site = $sites[ $slug ];
11015
11016 $addon_info['is_connected'] = (
11017 ( $addon->parent_plugin_id == $this->get_id() ) &&
11018 is_object( $site ) &&
11019 FS_Site::is_valid_id( $site->id ) &&
11020 FS_User::is_valid_id( $site->user_id ) &&
11021 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11022 );
11023
11024 if ( $addon_info['is_connected'] && $is_installed ) {
11025 return $addon_info;
11026 }
11027
11028 $addon_info['site'] = $site;
11029
11030 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11031 if ( isset( $plugins_data[ $slug ] ) ) {
11032 $plugin_data = $plugins_data[ $slug ];
11033
11034 $addon_info['version'] = $plugin_data->version;
11035 }
11036
11037 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11038 if ( isset( $all_plans[ $slug ] ) ) {
11039 $plans = $all_plans[ $slug ];
11040
11041 foreach ( $plans as $plan ) {
11042 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11043 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11044 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11045 break;
11046 }
11047 }
11048 }
11049
11050 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11051 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11052 foreach ( $licenses[ $addon_id ] as $license ) {
11053 if ( $license->id == $site->license_id ) {
11054 $addon_info['license'] = $license;
11055 break;
11056 }
11057 }
11058 }
11059
11060 if ( isset( $addon_info['license'] ) ) {
11061 if ( isset( $addon_storage->subscriptions ) &&
11062 ! empty( $addon_storage->subscriptions )
11063 ) {
11064 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11065
11066 foreach ( $addon_subscriptions as $subscription ) {
11067 if ( $subscription->license_id == $site->license_id ) {
11068 $addon_info['subscription'] = $subscription;
11069 break;
11070 }
11071 }
11072 }
11073 }
11074
11075 return $addon_info;
11076 }
11077
11078 /**
11079 * @author Vova Feldman (@svovaf)
11080 * @since 2.0.0
11081 *
11082 * @param number $user_id
11083 *
11084 * @return FS_User
11085 */
11086 static function _get_user_by_id( $user_id ) {
11087 self::$_static_logger->entrance( "user_id = {$user_id}" );
11088
11089 $users = self::get_all_users();
11090
11091 if ( is_array( $users ) ) {
11092 if ( isset( $users[ $user_id ] ) &&
11093 $users[ $user_id ] instanceof FS_User &&
11094 $user_id == $users[ $user_id ]->id
11095 ) {
11096 return $users[ $user_id ];
11097 }
11098
11099 // If user wasn't found by the key, iterate over all the users collection.
11100 foreach ( $users as $user ) {
11101 /**
11102 * @var FS_User $user
11103 */
11104 if ( $user_id == $user->id ) {
11105 return $user;
11106 }
11107 }
11108 }
11109
11110 return null;
11111 }
11112
11113 /**
11114 * Checks if a Freemius user_id is associated with a super-admin.
11115 *
11116 * @author Vova Feldman (@svovaf)
11117 * @since 2.0.0
11118 *
11119 * @param number $user_id
11120 *
11121 * @return bool
11122 */
11123 private static function is_super_admin( $user_id ) {
11124 $is_super_admin = false;
11125
11126 $user = self::_get_user_by_id( $user_id );
11127
11128 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11129 self::require_pluggable_essentials();
11130
11131 $wp_user = get_user_by( 'email', $user->email );
11132
11133 if ( $wp_user instanceof WP_User ) {
11134 $super_admins = get_super_admins();
11135 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11136 }
11137 }
11138
11139 return $is_super_admin;
11140 }
11141
11142 #----------------------------------------------------------------------------------
11143 #region Plans & Licensing
11144 #----------------------------------------------------------------------------------
11145
11146 /**
11147 * Check if running premium plugin code.
11148 *
11149 * @author Vova Feldman (@svovaf)
11150 * @since 1.0.5
11151 *
11152 * @return bool
11153 */
11154 function is_premium() {
11155 /**
11156 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11157 * `register_constructor_hooks` method.
11158 *
11159 * @author Leo Fajardo (@leorw)
11160 * @since 2.2.3
11161 */
11162 return is_object( $this->_plugin ) ?
11163 $this->_plugin->is_premium :
11164 false;
11165 }
11166
11167 /**
11168 * Get site's plan ID.
11169 *
11170 * @author Vova Feldman (@svovaf)
11171 * @since 1.0.2
11172 *
11173 * @return number
11174 */
11175 function get_plan_id() {
11176 return $this->_site->plan_id;
11177 }
11178
11179 /**
11180 * Get site's plan title.
11181 *
11182 * @author Vova Feldman (@svovaf)
11183 * @since 1.0.2
11184 *
11185 * @return string
11186 */
11187 function get_plan_title() {
11188 $plan = $this->get_plan();
11189
11190 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11191 }
11192
11193 /**
11194 * Get site's plan name.
11195 *
11196 * @author Vova Feldman (@svovaf)
11197 * @since 2.0.0
11198 *
11199 * @return string
11200 */
11201 function get_plan_name() {
11202 $plan = $this->get_plan();
11203
11204 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11205 }
11206
11207 /**
11208 * @author Vova Feldman (@svovaf)
11209 * @since 1.0.9
11210 *
11211 * @return FS_Plugin_Plan|false
11212 */
11213 function get_plan() {
11214 if ( ! is_object( $this->_site ) ) {
11215 return false;
11216 }
11217
11218 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11219 $this->_get_plan_by_id( $this->_site->plan_id ) :
11220 false;
11221 }
11222
11223 /**
11224 * @author Vova Feldman (@svovaf)
11225 * @since 1.0.3
11226 *
11227 * @return bool
11228 */
11229 function is_trial() {
11230 $this->_logger->entrance();
11231
11232 if ( ! $this->is_registered() || ! is_object( $this->_site ) ) {
11233 return false;
11234 }
11235
11236 return $this->_site->is_trial();
11237 }
11238
11239 /**
11240 * Check if currently in a trial with payment method (credit card or paypal).
11241 *
11242 * @author Vova Feldman (@svovaf)
11243 * @since 1.1.7
11244 *
11245 * @return bool
11246 */
11247 function is_paid_trial() {
11248 $this->_logger->entrance();
11249
11250 if ( ! $this->is_trial() ) {
11251 return false;
11252 }
11253
11254 if ( ! $this->has_active_valid_license() ) {
11255 return false;
11256 }
11257
11258 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11259 return false;
11260 }
11261
11262 /**
11263 * @var FS_Subscription $subscription
11264 */
11265 $subscription = $this->_get_subscription( $this->_license->id );
11266
11267 return ( is_object( $subscription ) && $subscription->is_active() );
11268 }
11269
11270 /**
11271 * Check if trial already utilized.
11272 *
11273 * @since 1.0.9
11274 *
11275 * @return bool
11276 */
11277 function is_trial_utilized() {
11278 $this->_logger->entrance();
11279
11280 if ( ! $this->is_registered() ) {
11281 return false;
11282 }
11283
11284 return $this->_site->is_trial_utilized();
11285 }
11286
11287 /**
11288 * Get trial plan information (if in trial).
11289 *
11290 * @author Vova Feldman (@svovaf)
11291 * @since 1.0.9
11292 *
11293 * @return bool|FS_Plugin_Plan
11294 */
11295 function get_trial_plan() {
11296 $this->_logger->entrance();
11297
11298 if ( ! $this->is_trial() ) {
11299 return false;
11300 }
11301
11302 // Try to load plan from local cache.
11303 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11304
11305 if ( ! is_object( $trial_plan ) ) {
11306 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11307
11308 /**
11309 * If managed to fetch the plan, add it to the plans collection.
11310 */
11311 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11312 if ( ! is_array( $this->_plans ) ) {
11313 $this->_plans = array();
11314 }
11315
11316 $this->_plans[] = $trial_plan;
11317 $this->_store_plans();
11318 }
11319 }
11320
11321 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11322 return $trial_plan;
11323 }
11324
11325 /**
11326 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11327 */
11328 $trial_plan = new FS_Plugin_Plan();
11329 $trial_plan->id = $this->_site->trial_plan_id;
11330 $trial_plan->name = 'pro';
11331 $trial_plan->title = 'Pro';
11332
11333 return $trial_plan;
11334 }
11335
11336 /**
11337 * Check if the user has an activate, non-expired license on current plugin's install.
11338 *
11339 * @since 1.0.9
11340 *
11341 * @return bool
11342 */
11343 function is_paying() {
11344 $this->_logger->entrance();
11345
11346 if ( ! $this->is_registered() ) {
11347 return false;
11348 }
11349
11350 if ( ! $this->has_paid_plan() ) {
11351 return false;
11352 }
11353
11354 return (
11355 ! $this->is_trial() &&
11356 'free' !== $this->get_plan_name() &&
11357 $this->has_active_valid_license()
11358 );
11359 }
11360
11361 /**
11362 * @author Vova Feldman (@svovaf)
11363 * @since 1.0.4
11364 *
11365 * @return bool
11366 */
11367 function is_free_plan() {
11368 if ( ! $this->is_registered() ) {
11369 return true;
11370 }
11371
11372 if ( ! $this->has_paid_plan() ) {
11373 return true;
11374 }
11375
11376 return (
11377 'free' === $this->get_plan_name() ||
11378 ! $this->has_features_enabled_license()
11379 );
11380 }
11381
11382 /**
11383 * @author Vova Feldman (@svovaf)
11384 * @since 1.0.5
11385 *
11386 * @return bool
11387 */
11388 function _has_premium_license() {
11389 $this->_logger->entrance();
11390
11391 $premium_license = $this->_get_available_premium_license();
11392
11393 return ( false !== $premium_license );
11394 }
11395
11396 /**
11397 * Check if user has any licenses associated with the plugin (including expired or blocking).
11398 *
11399 * @author Vova Feldman (@svovaf)
11400 * @since 1.1.7.3
11401 *
11402 * @param bool $including_foreign
11403 *
11404 * @return bool
11405 */
11406 function has_any_license( $including_foreign = true ) {
11407 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11408 return false;
11409 }
11410
11411 if ( $including_foreign ) {
11412 return true;
11413 }
11414
11415 foreach ( $this->_licenses as $license ) {
11416 if ( $this->_user->id == $license->user_id ) {
11417 return true;
11418 }
11419 }
11420
11421 return false;
11422 }
11423
11424 /**
11425 * @author Vova Feldman (@svovaf)
11426 * @since 1.0.5
11427 *
11428 * @param bool|null $is_localhost
11429 *
11430 * @return FS_Plugin_License|false
11431 */
11432 function _get_available_premium_license( $is_localhost = null ) {
11433 $this->_logger->entrance();
11434
11435 $licenses = $this->get_available_premium_licenses( $is_localhost );
11436 if ( ! empty( $licenses ) ) {
11437 return $licenses[0];
11438 }
11439
11440 return false;
11441 }
11442
11443 /**
11444 * @author Vova Feldman (@svovaf)
11445 * @since 1.0.5
11446 *
11447 * @param bool|null $is_localhost
11448 *
11449 * @return FS_Plugin_License[]
11450 */
11451 function get_available_premium_licenses( $is_localhost = null ) {
11452 $this->_logger->entrance();
11453
11454 $licenses = array();
11455 if ( ! $this->has_paid_plan() ) {
11456 return $licenses;
11457 }
11458
11459 if ( is_array( $this->_licenses ) ) {
11460 foreach ( $this->_licenses as $license ) {
11461 if ( ! $license->can_activate( $is_localhost ) ) {
11462 continue;
11463 }
11464
11465 $licenses[] = $license;
11466 }
11467 }
11468
11469 return $licenses;
11470 }
11471
11472 /**
11473 * Sync local plugin plans with remote server.
11474 *
11475 * 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).
11476 *
11477 * @author Vova Feldman (@svovaf)
11478 * @since 1.0.5
11479 *
11480 * @return FS_Plugin_Plan[]|object
11481 */
11482 function _sync_plans() {
11483 $plans = $this->_fetch_plugin_plans();
11484
11485 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11486 $plans_map = array();
11487 foreach ( $plans as $plan ) {
11488 $plans_map[ $plan->id ] = true;
11489 }
11490
11491 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11492
11493 foreach ( $plans_ids_to_keep as $plan_id ) {
11494 if ( isset( $plans_map[ $plan_id ] ) ) {
11495 continue;
11496 }
11497
11498 $missing_plan = self::_get_plan_by_id( $plan_id );
11499
11500 if ( is_object( $missing_plan ) ) {
11501 $plans[] = $missing_plan;
11502 }
11503 }
11504
11505 $this->_plans = $plans;
11506 $this->_store_plans();
11507 }
11508
11509 $this->do_action( 'after_plans_sync', $plans );
11510
11511 return $this->_plans;
11512 }
11513
11514 /**
11515 * Check if specified plan exists locally. If not, fetch it and store it.
11516 *
11517 * @author Vova Feldman (@svovaf)
11518 * @since 2.0.0
11519 *
11520 * @param number $plan_id
11521 *
11522 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11523 */
11524 private function sync_plan_if_not_exist( $plan_id ) {
11525 $plan = self::_get_plan_by_id( $plan_id );
11526
11527 if ( is_object( $plan ) ) {
11528 // Plan already exists.
11529 return $plan;
11530 }
11531
11532 $plan = $this->fetch_plan_by_id( $plan_id );
11533
11534 if ( $plan instanceof FS_Plugin_Plan ) {
11535 $this->_plans[] = $plan;
11536 $this->_store_plans();
11537
11538 return $plan;
11539 }
11540
11541 return $plan;
11542 }
11543
11544 /**
11545 * Check if specified license exists locally. If not, fetch it and store it.
11546 *
11547 * @author Vova Feldman (@svovaf)
11548 * @since 2.0.0
11549 *
11550 * @param number $license_id
11551 * @param string $license_key
11552 *
11553 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11554 */
11555 private function sync_license_if_not_exist( $license_id, $license_key ) {
11556 $license = $this->_get_license_by_id( $license_id );
11557
11558 if ( is_object( $license ) ) {
11559 // License already exists.
11560 return $license;
11561 }
11562
11563 $license = $this->fetch_license_by_key( $license_id, $license_key );
11564
11565 if ( $license instanceof FS_Plugin_License ) {
11566 $this->_licenses[] = $license;
11567
11568 $this->set_license( $license );
11569
11570 $this->_store_licenses();
11571
11572 return $license;
11573 }
11574
11575 return $license;
11576 }
11577
11578 /**
11579 * Get a collection of unique plan IDs that are associated with any installs in the network.
11580 *
11581 * @author Leo Fajardo (@leorw)
11582 * @since 2.0.0
11583 *
11584 * @return number[]
11585 */
11586 private function get_plans_ids_associated_with_installs() {
11587 if ( ! is_multisite() ) {
11588 if ( ! is_object( $this->_site ) ||
11589 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11590 ) {
11591 return array();
11592 }
11593
11594 return array( $this->_site->plan_id );
11595 }
11596
11597 $plan_ids = array();
11598 $sites = self::get_sites();
11599 foreach ( $sites as $site ) {
11600 $blog_id = self::get_site_blog_id( $site );
11601 $install = $this->get_install_by_blog_id( $blog_id );
11602
11603 if ( ! is_object( $install ) ||
11604 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11605 ) {
11606 continue;
11607 }
11608
11609 $plan_ids[ $install->plan_id ] = true;
11610 }
11611
11612 return array_keys( $plan_ids );
11613 }
11614
11615 /**
11616 * Get a collection of unique license IDs that are associated with any installs in the network.
11617 *
11618 * @author Leo Fajardo (@leorw)
11619 * @since 2.0.0
11620 *
11621 * @return number[]
11622 */
11623 private function get_license_ids_associated_with_installs() {
11624 if ( ! $this->_is_network_active ) {
11625 if ( ! is_object( $this->_site ) ||
11626 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11627 ) {
11628 return array();
11629 }
11630
11631 return array( $this->_site->license_id );
11632 }
11633
11634 $license_ids = array();
11635 $sites = self::get_sites();
11636 foreach ( $sites as $site ) {
11637 $blog_id = self::get_site_blog_id( $site );
11638 $install = $this->get_install_by_blog_id( $blog_id );
11639
11640 if ( ! is_object( $install ) ||
11641 ! FS_Plugin_License::is_valid_id( $install->license_id )
11642 ) {
11643 continue;
11644 }
11645
11646 $license_ids[ $install->license_id ] = true;
11647 }
11648
11649 return array_keys( $license_ids );
11650 }
11651
11652 /**
11653 * @author Vova Feldman (@svovaf)
11654 * @since 1.0.5
11655 *
11656 * @param number $id
11657 *
11658 * @return FS_Plugin_Plan|false
11659 */
11660 function _get_plan_by_id( $id ) {
11661 $this->_logger->entrance();
11662
11663 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11664 $this->_sync_plans();
11665 }
11666
11667 foreach ( $this->_plans as $plan ) {
11668 if ( $id == $plan->id ) {
11669 return $plan;
11670 }
11671 }
11672
11673 return false;
11674 }
11675
11676 /**
11677 * @author Vova Feldman (@svovaf)
11678 * @since 1.1.8.1
11679 *
11680 * @param string $name
11681 *
11682 * @return FS_Plugin_Plan|false
11683 */
11684 private function get_plan_by_name( $name ) {
11685 $this->_logger->entrance();
11686
11687 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11688 $this->_sync_plans();
11689 }
11690
11691 foreach ( $this->_plans as $plan ) {
11692 if ( $name == $plan->name ) {
11693 return $plan;
11694 }
11695 }
11696
11697 return false;
11698 }
11699
11700 /**
11701 * Sync local licenses with remote server.
11702 *
11703 * @author Vova Feldman (@svovaf)
11704 * @since 1.0.6
11705 *
11706 * @param number|bool $site_license_id
11707 * @param number|null $blog_id
11708 *
11709 * @return FS_Plugin_License[]|object
11710 */
11711 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11712 $this->_logger->entrance();
11713
11714 $is_network_admin = fs_is_network_admin();
11715
11716 if ( $is_network_admin && is_null( $blog_id ) ) {
11717 $all_licenses = self::get_all_licenses( $this->_module_id );
11718 } else {
11719 $all_licenses = $this->get_user_licenses( $this->_user->id );
11720 }
11721
11722 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11723
11724 $all_licenses_map = array();
11725 foreach ( $all_licenses as $license ) {
11726 $all_licenses_map[ $license->id ] = true;
11727 }
11728
11729 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11730
11731 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11732 $licenses_map = array();
11733 foreach ( $licenses as $license ) {
11734 $licenses_map[ $license->id ] = true;
11735 }
11736
11737 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11738 // foreach ( $license_ids_to_keep as $license_id ) {
11739 // if ( isset( $licenses_map[ $license_id ] ) ) {
11740 // continue;
11741 // }
11742 //
11743 // $missing_license = self::_get_license_by_id( $license_id, false );
11744 // if ( is_object( $missing_license ) ) {
11745 // $licenses[] = $missing_license;
11746 // $licenses_map[ $missing_license->id ] = true;
11747 // }
11748 // }
11749
11750 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11751
11752 foreach ( $user_license_ids as $key => $license_id ) {
11753 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11754 // Remove access to licenses that no longer exist.
11755 unset( $user_license_ids[ $key ] );
11756 }
11757 }
11758
11759 if ( ! empty( $user_license_ids ) ) {
11760 foreach ( $licenses_map as $license_id => $value ) {
11761 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11762 // Associate new licenses with the user who triggered the license syncing.
11763 $user_license_ids[] = $license_id;
11764 }
11765 }
11766
11767 $user_license_ids = array_unique( $user_license_ids );
11768 } else {
11769 $user_license_ids = array_keys( $licenses_map );
11770 }
11771
11772 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11773 $user_licenses = array();
11774 foreach ( $licenses as $license ) {
11775 if ( ! in_array( $license->id, $user_license_ids ) ) {
11776 continue;
11777 }
11778
11779 $user_licenses[] = $license;
11780 }
11781
11782 $this->_licenses = $user_licenses;
11783 } else {
11784 $this->_licenses = $licenses;
11785 }
11786
11787 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11788
11789 $this->_store_licenses( true, $this->_module_id, $licenses );
11790 }
11791
11792 // Update current license.
11793 if ( is_object( $this->_license ) ) {
11794 $license = $this->_get_license_by_id( $this->_license->id );
11795
11796 if ( is_object( $license ) ) {
11797 /**
11798 * `$license` can be `false` in case a user change action has just been completed and this method
11799 * has synced the `$this->_licenses` collection for the new user. In this case, the
11800 * `$this->_licenses` collection may have only the newly activated license that is associated with
11801 * the new user. `set_license` will eventually be called in the same request by the logic that
11802 * follows outside this method which will detect that the install's license has been updated, and
11803 * then `_update_site_license` will be called which in turn will call `set_license`.
11804 *
11805 * @author Leo Fajardo (@leorw)
11806 * @since 2.3.2
11807 */
11808 $this->set_license( $license );
11809 }
11810 }
11811
11812 return $this->_licenses;
11813 }
11814
11815 /**
11816 * @author Vova Feldman (@svovaf)
11817 * @since 1.0.5
11818 *
11819 * @param number $id
11820 * @param bool $sync_licenses
11821 *
11822 * @return FS_Plugin_License|false
11823 */
11824 function _get_license_by_id( $id, $sync_licenses = true ) {
11825 $this->_logger->entrance();
11826
11827 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11828 return false;
11829 }
11830
11831 /**
11832 * When running from the network level admin and opted-in from the network,
11833 * check if the license exists in the network user licenses collection.
11834 *
11835 * @author Vova Feldman (@svovaf)
11836 * @since 2.0.0
11837 */
11838 if ( fs_is_network_admin() &&
11839 $this->is_network_registered() &&
11840 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11841 ) {
11842 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11843
11844 foreach ( $licenses as $license ) {
11845 if ( $id == $license->id ) {
11846 return $license;
11847 }
11848 }
11849 }
11850
11851 if ( ! $this->has_any_license() && $sync_licenses ) {
11852 $this->_sync_licenses( $id );
11853 }
11854
11855 if ( is_array( $this->_licenses ) ) {
11856 foreach ( $this->_licenses as $license ) {
11857 if ( $id == $license->id ) {
11858 return $license;
11859 }
11860 }
11861 }
11862
11863 return false;
11864 }
11865
11866 /**
11867 * 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.
11868 *
11869 * @author Vova Feldman (@svovaf)
11870 * @since 2.0.0
11871 *
11872 * @param number $id
11873 *
11874 * @return FS_Plugin_License
11875 */
11876 private function get_license_by_id( $id ) {
11877 $licenses = self::get_all_licenses( $this->_module_id );
11878
11879 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11880 foreach ( $licenses as $license ) {
11881 if ( $id == $license->id ) {
11882 return $license;
11883 }
11884 }
11885 }
11886
11887 return null;
11888 }
11889
11890 /**
11891 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11892 *
11893 * @author Vova Feldman (@svovaf)
11894 * @since 2.0.0
11895 *
11896 * @return \FS_Plugin_License|mixed
11897 */
11898 private function sync_site_license() {
11899 $api = $this->get_api_user_scope();
11900
11901 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11902
11903 if ( ! $this->is_api_result_entity( $result ) ) {
11904 return $result;
11905 }
11906
11907 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11908 $this->_store_licenses();
11909
11910 return $license;
11911 }
11912
11913 /**
11914 * Get all user's available licenses for the current module.
11915 *
11916 * @author Vova Feldman (@svovaf)
11917 * @since 2.0.0
11918 *
11919 * @param number $user_id
11920 *
11921 * @return FS_Plugin_License[]
11922 */
11923 private function get_user_licenses( $user_id ) {
11924 $all_licenses = self::get_all_licenses( $this->_module_id );
11925 if ( empty( $all_licenses ) ) {
11926 return array();
11927 }
11928
11929 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
11930 if ( empty( $user_license_ids ) ) {
11931 return array();
11932 }
11933
11934 $licenses = array();
11935 foreach ( $all_licenses as $license ) {
11936 if ( in_array( $license->id, $user_license_ids ) ) {
11937 $licenses[] = $license;
11938 }
11939 }
11940
11941 return $licenses;
11942 }
11943
11944 /**
11945 * Checks if the context license is network activated except on the given blog ID.
11946 *
11947 * @author Vova Feldman (@svovaf)
11948 * @since 2.0.0
11949 *
11950 * @param int $except_blog_id
11951 *
11952 * @return bool
11953 */
11954 private function is_license_network_active( $except_blog_id = 0 ) {
11955 $this->_logger->entrance();
11956
11957 if ( ! is_object( $this->_license ) ) {
11958 return false;
11959 }
11960
11961 $sites = self::get_sites();
11962
11963 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
11964 // There are more sites than the number of activations, so license cannot be network activated.
11965 return false;
11966 }
11967
11968 foreach ( $sites as $site ) {
11969 $blog_id = self::get_site_blog_id( $site );
11970
11971 if ( $except_blog_id == $blog_id ) {
11972 // Skip excluded blog.
11973 continue;
11974 }
11975
11976 $install = $this->get_install_by_blog_id( $blog_id );
11977
11978 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
11979 return false;
11980 }
11981 }
11982
11983 return true;
11984 }
11985
11986 /**
11987 * 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.
11988 *
11989 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
11990 *
11991 * @author Vova Feldman (@svovaf)
11992 * @since 2.0.0
11993 *
11994 * @param \FS_User $user
11995 * @param \FS_Plugin_License $license
11996 *
11997 * @return bool
11998 */
11999 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12000 $this->_logger->entrance();
12001
12002 $result = $this->can_activate_license_on_network( $license );
12003
12004 if ( false === $result ) {
12005 return false;
12006 }
12007
12008 $installs_without_license = $result['installs'];
12009 if ( ! empty( $installs_without_license ) ) {
12010 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12011 }
12012
12013 $disconnected_site_ids = $result['sites'];
12014 if ( ! empty( $disconnected_site_ids ) ) {
12015 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12016 }
12017
12018 $this->link_license_2_user( $license->id, $user->id );
12019
12020 // Sync license after activations.
12021 $license->activated += $result['production_count'];
12022 $license->activated_local += $result['localhost_count'];
12023
12024 // $this->_store_licenses()
12025
12026 return true;
12027 }
12028
12029 /**
12030 * Checks if the given license can be activated on the whole network.
12031 *
12032 * @author Vova Feldman (@svovaf)
12033 * @since 2.0.0
12034 *
12035 * @param \FS_Plugin_License $license
12036 *
12037 * @return false|array {
12038 * @type array[int]FS_Site $installs Blog ID to install map.
12039 * @type int[] $sites Non-connected blog IDs.
12040 * @type int $production_count Production sites count.
12041 * @type int $localhost_count Production sites count.
12042 * }
12043 */
12044 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12045 $sites = self::get_sites();
12046
12047 $production_count = 0;
12048 $localhost_count = 0;
12049
12050 $installs_without_license = array();
12051 $disconnected_site_ids = array();
12052
12053 foreach ( $sites as $site ) {
12054 $blog_id = self::get_site_blog_id( $site );
12055 $install = $this->get_install_by_blog_id( $blog_id );
12056
12057 if ( is_object( $install ) ) {
12058 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12059 // License already activated on the install.
12060 continue;
12061 }
12062
12063 $url = $install->url;
12064
12065 $installs_without_license[ $blog_id ] = $install;
12066 } else {
12067 $url = is_object( $site ) ?
12068 $site->siteurl :
12069 get_site_url( $blog_id );
12070
12071 $disconnected_site_ids[] = $blog_id;
12072 }
12073
12074 if ( FS_Site::is_localhost_by_address( $url ) ) {
12075 $localhost_count ++;
12076 } else {
12077 $production_count ++;
12078 }
12079 }
12080
12081 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12082 return false;
12083 }
12084
12085 return array(
12086 'installs' => $installs_without_license,
12087 'sites' => $disconnected_site_ids,
12088 'production_count' => $production_count,
12089 'localhost_count' => $localhost_count,
12090 );
12091 }
12092
12093 /**
12094 * Activate a given license on a collection of installs.
12095 *
12096 * @author Vova Feldman (@svovaf)
12097 * @since 2.0.0
12098 *
12099 * @param \FS_User $user
12100 * @param string $license_key
12101 * @param array $blog_2_install_map {
12102 * @key int Blog ID.
12103 * @value FS_Site Blog's associated install.
12104 * }
12105 *
12106 * @return mixed|true
12107 */
12108 private function activate_license_on_many_installs(
12109 FS_User $user,
12110 $license_key,
12111 array $blog_2_install_map
12112 ) {
12113 $params = array(
12114 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12115 );
12116
12117 $install_2_blog_map = array();
12118 foreach ( $blog_2_install_map as $blog_id => $install ) {
12119 $params[] = array( 'id' => $install->id );
12120
12121 $install_2_blog_map[ $install->id ] = $blog_id;
12122 }
12123
12124 $result = $this->get_api_user_scope_by_user( $user )->call(
12125 "plugins/{$this->_plugin->id}/installs.json",
12126 'PUT',
12127 $params
12128 );
12129
12130 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12131 return $result;
12132 }
12133
12134 foreach ( $result->installs as $r_install ) {
12135 $install = new FS_Site( $r_install );
12136 $install->is_disconnected = false;
12137
12138 // Update install.
12139 $this->_store_site(
12140 true,
12141 $install_2_blog_map[ $r_install->id ],
12142 $install
12143 );
12144 }
12145
12146 return true;
12147 }
12148
12149 /**
12150 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12151 *
12152 * @author Vova Feldman (@svovaf)
12153 * @since 2.3.1
12154 *
12155 * @param \FS_User $user
12156 * @param string $license_key
12157 *
12158 * @return true|mixed True if successful, otherwise, the API result.
12159 */
12160 private function activate_license_on_site( FS_User $user, $license_key ) {
12161 return $this->activate_license_on_many_sites( $user, $license_key );
12162 }
12163
12164 /**
12165 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12166 *
12167 * @author Vova Feldman (@svovaf)
12168 * @since 2.0.0
12169 *
12170 * @param \FS_User $user
12171 * @param string $license_key
12172 * @param int[] $site_ids
12173 *
12174 * @return true|mixed True if successful, otherwise, the API result.
12175 */
12176 private function activate_license_on_many_sites(
12177 FS_User $user,
12178 $license_key,
12179 array $site_ids = array()
12180 ) {
12181 $sites = array();
12182 foreach ( $site_ids as $site_id ) {
12183 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12184 }
12185
12186 // Install the plugin.
12187 $result = $this->create_installs_with_user(
12188 $user,
12189 $license_key,
12190 false,
12191 $sites,
12192 false,
12193 true
12194 );
12195
12196 if ( ! $this->is_api_result_entity( $result ) &&
12197 ! $this->is_api_result_object( $result, 'installs' )
12198 ) {
12199 return $result;
12200 }
12201
12202 $installs = array();
12203
12204 if ( $this->is_api_result_entity( $result ) ) {
12205 $install = new FS_Site( $result );
12206
12207 $this->_user = $user;
12208
12209 $this->_store_site( true, null, $install );
12210
12211 $this->_site = $install;
12212
12213 $this->reset_anonymous_mode();
12214 } else {
12215 foreach ( $result->installs as $install ) {
12216 $installs[] = new FS_Site( $install );
12217 }
12218
12219 // Map site addresses to their blog IDs.
12220 $address_to_blog_map = $this->get_address_to_blog_map();
12221
12222 $first_blog_id = null;
12223
12224 foreach ( $installs as $install ) {
12225 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12226 $blog_id = $address_to_blog_map[ $address ];
12227
12228 $this->_store_site( true, $blog_id, $install );
12229
12230 $this->reset_anonymous_mode( $blog_id );
12231
12232 if ( is_null( $first_blog_id ) ) {
12233 $first_blog_id = $blog_id;
12234 }
12235 }
12236
12237 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12238 $this->_storage->network_install_blog_id = $first_blog_id;
12239 }
12240 }
12241
12242 return true;
12243 }
12244
12245 /**
12246 * Sync site's license with user licenses.
12247 *
12248 * @author Vova Feldman (@svovaf)
12249 * @since 1.0.6
12250 *
12251 * @param FS_Plugin_License|null $new_license
12252 *
12253 * @return FS_Plugin_License|null
12254 */
12255 function _update_site_license( $new_license ) {
12256 $this->_logger->entrance();
12257
12258 /**
12259 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12260 * accordingly so that it will also handle the case when an ownership change is done via license
12261 * activation.
12262 *
12263 * @author Leo Fajardo (@leorw)
12264 * @since 2.3.2
12265 */
12266 $this->set_license( $new_license );
12267
12268 if ( ! is_object( $new_license ) ) {
12269 $this->_site->license_id = null;
12270 $this->_sync_site_subscription( null );
12271
12272 return $this->_license;
12273 }
12274
12275 $this->_site->license_id = $this->_license->id;
12276
12277 if ( ! is_array( $this->_licenses ) ) {
12278 $this->_licenses = array();
12279 }
12280
12281 $is_license_found = false;
12282 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12283 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12284 $this->_licenses[ $i ] = $new_license;
12285
12286 $is_license_found = true;
12287 break;
12288 }
12289 }
12290
12291 // If new license just append.
12292 if ( ! $is_license_found ) {
12293 $this->_licenses[] = $new_license;
12294 }
12295
12296 $this->_sync_site_subscription( $new_license );
12297
12298 return $this->_license;
12299 }
12300
12301 /**
12302 * @author Vova Feldman (@svovaf)
12303 * @since 2.3.1
12304 *
12305 * @param \FS_Plugin_License $license
12306 */
12307 private function set_license( FS_Plugin_License $license = null ) {
12308 $this->_license = $license;
12309
12310 $this->maybe_update_whitelabel_flag( $license );
12311 }
12312
12313 /**
12314 * @author Leo Fajardo (@leorw)
12315 * @since 2.3.1
12316 *
12317 * @param FS_Plugin_License $license
12318 */
12319 private function maybe_update_whitelabel_flag( $license ) {
12320 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12321 $this->_storage->is_whitelabeled :
12322 false;
12323
12324 if ( is_object( $license ) ) {
12325 $license_user = self::_get_user_by_id( $license->user_id );
12326
12327 if ( ! is_object( $license_user ) ) {
12328 // If foreign license, do not update the `is_whitelabeled` flag.
12329 return;
12330 }
12331
12332 if ( $this->is_addon() ) {
12333 /**
12334 * Store the last license data to the parent's storage since it's needed only when showing the
12335 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12336 * iterate over the add-ons just to get the last license data.
12337 */
12338 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12339 } else {
12340 $this->store_last_activated_license_data( $license );
12341 }
12342
12343 if ( $license->is_whitelabeled ) {
12344 // Activated a developer license, data should be hidden.
12345 $is_whitelabeled = true;
12346 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12347 // The account owner activated a regular license key, no need to hide the data.
12348 $is_whitelabeled = false;
12349 }
12350 }
12351
12352 $this->_storage->is_whitelabeled = $is_whitelabeled;
12353
12354 // Reset the whitelabeled status after update.
12355 $this->is_whitelabeled = null;
12356 if ( $this->is_addon() ) {
12357 $parent_fs = $this->get_parent_instance();
12358
12359 if ( is_object( $parent_fs ) ) {
12360 $parent_fs->is_whitelabeled = null;
12361 }
12362 }
12363 }
12364
12365 /**
12366 * @author Leo Fajardo (@leorw)
12367 * @since 2.3.1
12368 *
12369 * @param FS_Plugin_License $license
12370 * @param FS_User $license_user
12371 */
12372 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12373 if ( ! is_object( $license_user ) ) {
12374 $this->_storage->last_license_key = md5( $license->secret_key );
12375 $this->_storage->last_license_user_id = null;
12376 } else {
12377 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12378 $this->_storage->last_license_user_id = $license_user->id;
12379 }
12380 }
12381
12382 /**
12383 * @author Leo Fajardo (@leorw)
12384 * @since 2.3.1
12385 *
12386 * @param bool $ignore_data_debug_mode
12387 *
12388 * @return bool
12389 */
12390 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12391 if ( true !== $this->_storage->is_whitelabeled ) {
12392 return false;
12393 } else if ( $ignore_data_debug_mode ) {
12394 return true;
12395 }
12396
12397 $fs = $this->is_addon() ?
12398 $this->get_parent_instance() :
12399 $this;
12400
12401 return ! $fs->is_data_debug_mode();
12402 }
12403
12404 /**
12405 * @author Leo Fajardo (@leorw)
12406 * @since 2.3.1
12407 *
12408 * @return number
12409 */
12410 function get_last_license_user_id() {
12411 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12412 $this->_storage->last_license_user_id :
12413 null;
12414 }
12415
12416 /**
12417 * @author Leo Fajardo (@leorw)
12418 * @since 2.3.1
12419 *
12420 * @param int $blog_id
12421 * @param bool $ignore_data_debug_mode
12422 *
12423 * @return bool
12424 */
12425 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12426 if ( ! is_null( $blog_id ) ) {
12427 $this->switch_to_blog( $blog_id );
12428 }
12429
12430 if ( ! is_null( $this->is_whitelabeled ) ) {
12431 $is_whitelabeled = $this->is_whitelabeled;
12432 } else {
12433 $is_whitelabeled = false;
12434
12435 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12436
12437 if ( ! $this->has_addons() ) {
12438 $is_whitelabeled = $is_whitelabeled_flag;
12439 } else if ( $is_whitelabeled_flag ) {
12440 $is_whitelabeled = true;
12441 } else {
12442 $addon_ids = $this->get_updated_account_addons();
12443 $installed_addons = $this->get_installed_addons();
12444 foreach ( $installed_addons as $fs_addon ) {
12445 $addon_ids[] = $fs_addon->get_id();
12446 }
12447
12448 if ( ! empty( $addon_ids ) ) {
12449 $addon_ids = array_unique( $addon_ids );
12450
12451 $is_network_level = (
12452 fs_is_network_admin() &&
12453 $this->is_network_active()
12454 );
12455
12456 foreach ( $addon_ids as $addon_id ) {
12457 $addon = $this->get_addon( $addon_id );
12458
12459 if ( ! is_object( $addon ) ) {
12460 continue;
12461 }
12462
12463 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12464 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12465 self::get_addon_instance( $addon_id ) :
12466 null;
12467
12468 $was_addon_network_activated = false;
12469
12470 if ( is_object( $fs_addon ) ) {
12471 $was_addon_network_activated = $fs_addon->is_network_active();
12472 } else if ( $is_network_level ) {
12473 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12474 }
12475
12476 $network_delegated_connection = (
12477 $was_addon_network_activated &&
12478 $addon_storage->get( 'is_delegated_connection', false, true )
12479 );
12480
12481 if (
12482 $is_network_level &&
12483 ( ! $was_addon_network_activated || $network_delegated_connection )
12484 ) {
12485 $sites = self::get_sites();
12486
12487 /**
12488 * If in network admin area and the add-on was not network-activated or network-activated
12489 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12490 */
12491 foreach ( $sites as $site ) {
12492 $site_info = $this->get_site_info( $site );
12493
12494 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12495 $is_whitelabeled = true;
12496 break;
12497 }
12498 }
12499
12500 if ( $is_whitelabeled ) {
12501 break;
12502 }
12503 } else {
12504 /**
12505 * This will be executed when any of the following is met:
12506 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12507 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12508 * 3. Add-on was not network-activated and in site admin area.
12509 */
12510 if ( true === $addon_storage->is_whitelabeled ) {
12511 $is_whitelabeled = true;
12512 break;
12513 }
12514 }
12515 }
12516 }
12517 }
12518
12519 $this->is_whitelabeled = $is_whitelabeled;
12520
12521 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12522 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12523 }
12524
12525 if ( ! is_null( $blog_id ) ) {
12526 $this->restore_current_blog();
12527 }
12528 }
12529
12530 return (
12531 $is_whitelabeled &&
12532 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12533 );
12534 }
12535
12536 /**
12537 * Sync site's subscription.
12538 *
12539 * @author Vova Feldman (@svovaf)
12540 * @since 1.0.9
12541 *
12542 * @param FS_Plugin_License|null $license
12543 *
12544 * @return bool|\FS_Subscription
12545 */
12546 private function _sync_site_subscription( $license ) {
12547 if ( ! is_object( $license ) ) {
12548 $this->delete_unused_subscriptions();
12549
12550 return false;
12551 }
12552
12553 // Load subscription details if not lifetime.
12554 $subscription = $license->is_lifetime() ?
12555 false :
12556 $this->_fetch_site_license_subscription();
12557
12558 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12559 $this->store_subscription( $subscription );
12560 } else {
12561 $this->delete_unused_subscriptions();
12562 }
12563
12564 return $subscription;
12565 }
12566
12567 /**
12568 * @author Vova Feldman (@svovaf)
12569 * @since 1.0.6
12570 *
12571 * @return bool|\FS_Plugin_License
12572 */
12573 function _get_license() {
12574 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12575 return $this->_license;
12576 }
12577
12578 return $this->_get_available_premium_license();
12579 }
12580
12581 /**
12582 * @param number $license_id
12583 *
12584 * @return null|\FS_Subscription
12585 */
12586 function _get_subscription( $license_id ) {
12587 if ( ! isset( $this->_storage->subscriptions ) ||
12588 empty( $this->_storage->subscriptions )
12589 ) {
12590 return null;
12591 }
12592
12593 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12594 if ( $subscription->license_id == $license_id ) {
12595 return $subscription;
12596 }
12597 }
12598
12599 return null;
12600 }
12601
12602 /**
12603 * @author Leo Fajardo (@leorw)
12604 * @since 2.0.0
12605 *
12606 * @param FS_Subscription $subscription
12607 */
12608 function store_subscription( FS_Subscription $subscription ) {
12609 if ( ! isset( $this->_storage->subscriptions ) ) {
12610 $this->_storage->subscriptions = array();
12611 }
12612
12613 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12614 $this->_storage->subscriptions = array( $subscription );
12615
12616 return;
12617 }
12618
12619 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12620
12621 $updated_subscription = false;
12622 foreach ( $subscriptions as $key => $existing_subscription ) {
12623 if ( $existing_subscription->id == $subscription->id ) {
12624 $subscriptions[ $key ] = $subscription;
12625 $updated_subscription = true;
12626 break;
12627 }
12628 }
12629
12630 if ( ! $updated_subscription ) {
12631 $subscriptions[] = $subscription;
12632 }
12633
12634 $this->_storage->subscriptions = $subscriptions;
12635 }
12636
12637 /**
12638 * @author Leo Fajardo (@leorw)
12639 * @since 2.0.0
12640 */
12641 function delete_unused_subscriptions() {
12642 if ( ! isset( $this->_storage->subscriptions ) ||
12643 empty( $this->_storage->subscriptions ) ||
12644 // Clean up only if there are already at least 3 subscriptions.
12645 ( count( $this->_storage->subscriptions ) < 3 )
12646 ) {
12647 return;
12648 }
12649
12650 if ( ! is_multisite() ) {
12651 // If not multisite, there should only be 1 subscription, so just clear the array.
12652 $this->_storage->subscriptions = array();
12653
12654 return;
12655 }
12656
12657 $subscriptions_to_keep_by_license_id_map = array();
12658 $sites = self::get_sites();
12659 foreach ( $sites as $site ) {
12660 $blog_id = self::get_site_blog_id( $site );
12661 $install = $this->get_install_by_blog_id( $blog_id );
12662
12663 if ( ! is_object( $install ) ||
12664 ! FS_Plugin_License::is_valid_id( $install->license_id )
12665 ) {
12666 continue;
12667 }
12668
12669 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12670 }
12671
12672 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12673 $this->_storage->subscriptions = array();
12674
12675 return;
12676 }
12677
12678 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12679 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12680 unset( $this->_storage->subscriptions[ $key ] );
12681 }
12682 }
12683 }
12684
12685 /**
12686 * @author Vova Feldman (@svovaf)
12687 * @since 1.0.2
12688 *
12689 * @param string $plan Plan name
12690 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12691 *
12692 * @return bool
12693 */
12694 function is_plan( $plan, $exact = false ) {
12695 $this->_logger->entrance();
12696
12697 if ( ! $this->is_registered() ) {
12698 return false;
12699 }
12700
12701 $plan = strtolower( $plan );
12702
12703 $current_plan_name = $this->get_plan_name();
12704
12705 if ( $current_plan_name === $plan ) {
12706 // Exact plan.
12707 return true;
12708 } else if ( $exact ) {
12709 // Required exact, but plans are different.
12710 return false;
12711 }
12712
12713 $current_plan_order = - 1;
12714 $required_plan_order = PHP_INT_MAX;
12715 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12716 if ( $plan === $this->_plans[ $i ]->name ) {
12717 $required_plan_order = $i;
12718 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12719 $current_plan_order = $i;
12720 }
12721 }
12722
12723 return ( $current_plan_order > $required_plan_order );
12724 }
12725
12726 /**
12727 * Check if module has only one plan.
12728 *
12729 * @author Vova Feldman (@svovaf)
12730 * @since 1.2.1.7
12731 *
12732 * @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.
12733 *
12734 * @return bool
12735 */
12736 function is_single_plan( $double_check = false ) {
12737 $this->_logger->entrance();
12738
12739 if ( ! $this->is_registered() ||
12740 ! is_array( $this->_plans ) ||
12741 0 === count( $this->_plans )
12742 ) {
12743 return true;
12744 }
12745
12746 $has_free_plan = $this->has_free_plan();
12747
12748 if ( ! $has_free_plan && $double_check ) {
12749 foreach ( $this->_plans as $plan ) {
12750 if ( $plan->is_free() ) {
12751 $has_free_plan = true;
12752 break;
12753 }
12754 }
12755 }
12756
12757 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12758 }
12759
12760 /**
12761 * Check if plan based on trial. If not in trial mode, should return false.
12762 *
12763 * @since 1.0.9
12764 *
12765 * @param string $plan Plan name
12766 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12767 *
12768 * @return bool
12769 */
12770 function is_trial_plan( $plan, $exact = false ) {
12771 $this->_logger->entrance();
12772
12773 if ( ! $this->is_registered() ) {
12774 return false;
12775 }
12776
12777 if ( ! $this->is_trial() ) {
12778 return false;
12779 }
12780
12781 $trial_plan = $this->get_trial_plan();
12782
12783 if ( $trial_plan->name === $plan ) {
12784 // Exact plan.
12785 return true;
12786 } else if ( $exact ) {
12787 // Required exact, but plans are different.
12788 return false;
12789 }
12790
12791 $current_plan_order = - 1;
12792 $required_plan_order = - 1;
12793 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12794 if ( $plan === $this->_plans[ $i ]->name ) {
12795 $required_plan_order = $i;
12796 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12797 $current_plan_order = $i;
12798 }
12799 }
12800
12801 return ( $current_plan_order > $required_plan_order );
12802 }
12803
12804 /**
12805 * Check if plugin has any paid plans.
12806 *
12807 * @author Vova Feldman (@svovaf)
12808 * @since 1.0.7
12809 *
12810 * @return bool
12811 */
12812 function has_paid_plan() {
12813 return $this->_has_paid_plans ||
12814 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12815 }
12816
12817 /**
12818 * Check if plugin has any plan with a trail.
12819 *
12820 * @author Vova Feldman (@svovaf)
12821 * @since 1.0.9
12822 *
12823 * @return bool
12824 */
12825 function has_trial_plan() {
12826 /**
12827 * @author Vova Feldman(@svovaf)
12828 * @since 1.2.1.5
12829 *
12830 * Allow setting a trial from the SDK without calling the API.
12831 * But, if the user did opt-in, continue using the real data from the API.
12832 */
12833 if ( $this->_trial_days >= 0 ) {
12834 return true;
12835 }
12836
12837 return $this->_storage->get( 'has_trial_plan', false );
12838 }
12839
12840 /**
12841 * Check if plugin has any free plan, or is it premium only.
12842 *
12843 * Note: If no plans configured, assume plugin is free.
12844 *
12845 * @author Vova Feldman (@svovaf)
12846 * @since 1.0.7
12847 *
12848 * @return bool
12849 */
12850 function has_free_plan() {
12851 return ! $this->is_only_premium();
12852 }
12853
12854 /**
12855 * Displays a license activation dialog box when the user clicks on the "Activate License"
12856 * or "Change License" link on the plugins
12857 * page.
12858 *
12859 * @author Leo Fajardo (@leorw)
12860 * @since 1.1.9
12861 */
12862 function _add_license_activation_dialog_box() {
12863 $vars = array(
12864 'id' => $this->_module_id,
12865 );
12866
12867 fs_require_template( 'forms/license-activation.php', $vars );
12868 fs_require_template( 'forms/resend-key.php', $vars );
12869 }
12870
12871 /**
12872 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
12873 *
12874 * @author Leo Fajardo (@leorw)
12875 * @since 2.3.2
12876 *
12877 * @return number[]
12878 */
12879 function get_installs_ids_with_foreign_licenses() {
12880 $installs = array();
12881
12882 if (
12883 is_object( $this->_license ) &&
12884 $this->_site->user_id != $this->_license->user_id
12885 ) {
12886 $installs[] = $this->_site->id;
12887 }
12888
12889 /**
12890 * Also try to get foreign licenses for the context product's add-ons.
12891 */
12892 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
12893
12894 foreach ( $installs_by_slug_map as $slug => $install_info ) {
12895 if ( $slug == $this->get_slug() ) {
12896 continue;
12897 }
12898
12899 $install = $install_info['install'];
12900 $license = $install_info['license'];
12901
12902 if (
12903 is_object( $license ) &&
12904 $install->user_id != $license->user_id
12905 ) {
12906 $installs[] = $install->id;
12907 }
12908 }
12909
12910 return $installs;
12911 }
12912
12913 /**
12914 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
12915 *
12916 * @author Leo Fajardo (@leorw)
12917 * @since 2.3.2
12918 *
12919 * @param number[] $install_ids
12920 */
12921 function _add_user_change_dialog_box( $install_ids ) {
12922 $vars = array(
12923 'id' => $this->_module_id,
12924 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
12925 );
12926
12927 fs_require_template( 'forms/user-change.php', $vars );
12928 }
12929
12930 /**
12931 * @author Leo Fajardo (@leorw)
12932 * @since 2.3.1
12933 */
12934 function _add_data_debug_mode_dialog_box() {
12935 $vars = array(
12936 'id' => $this->_module_id,
12937 );
12938
12939 fs_require_template( 'forms/data-debug-mode.php', $vars );
12940 }
12941
12942 /**
12943 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
12944 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
12945 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
12946 * is only activated on a single production site.
12947 *
12948 * @author Leo Fajardo (@leorw)
12949 * @since 2.2.1
12950 *
12951 * @param bool $is_license_deactivation
12952 *
12953 * @return array
12954 */
12955 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
12956 if ( fs_is_network_admin() ) {
12957 // Subscription cancellation dialog box is currently not supported for multisite networks.
12958 return array();
12959 }
12960
12961 if ( $this->is_whitelabeled() ) {
12962 return array();
12963 }
12964
12965 $license = $this->_get_license();
12966
12967 /**
12968 * 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.
12969 *
12970 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
12971 * @since 2.2.1
12972 */
12973 if ( ! is_object( $license ) ||
12974 $license->is_lifetime() ||
12975 ( ! $license->is_single_site() && $license->activated > 1 )
12976 ) {
12977 return array();
12978 }
12979
12980 /**
12981 * @var FS_Subscription $subscription
12982 */
12983 $subscription = $this->_get_subscription( $license->id );
12984 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
12985 return array();
12986 }
12987
12988 return array(
12989 'id' => $this->_module_id,
12990 'license' => $license,
12991 'has_trial' => $this->is_paid_trial(),
12992 'is_license_deactivation' => $is_license_deactivation,
12993 );
12994 }
12995
12996 /**
12997 * @author Leo Fajardo (@leorw)
12998 * @since 2.0.2
12999 */
13000 function _add_premium_version_upgrade_selection_dialog_box() {
13001 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13002 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13003 return;
13004 }
13005
13006 $vars = array(
13007 'id' => $this->_module_id,
13008 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13009 $modules_update->response[ $this->_plugin_basename ]->new_version :
13010 $modules_update->response[ $this->_plugin_basename ]['new_version']
13011 );
13012
13013 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13014 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13015 }
13016
13017 /**
13018 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13019 * page.
13020 *
13021 * @author Leo Fajardo (@leorw)
13022 * @since 1.2.1.5
13023 */
13024 function _add_optout_dialog() {
13025 if ( $this->is_theme() ) {
13026 $vars = null;
13027 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13028 }
13029
13030 $vars = array( 'id' => $this->_module_id );
13031 fs_require_template( 'forms/optout.php', $vars );
13032 }
13033
13034 /**
13035 * Prepare page to include all required UI and logic for the license activation dialog.
13036 *
13037 * @author Vova Feldman (@svovaf)
13038 * @since 1.2.0
13039 */
13040 function _add_license_activation() {
13041 if ( $this->is_migration() ) {
13042 return;
13043 }
13044
13045 if ( ! $this->is_user_admin() ) {
13046 // Only admins can activate a license.
13047 return;
13048 }
13049
13050 if ( ! $this->has_paid_plan() ) {
13051 // Module doesn't have any paid plans.
13052 return;
13053 }
13054
13055 if (
13056 $this->has_premium_version() &&
13057 ! $this->is_premium() &&
13058 /**
13059 * Also handle the case when an upgrade was made using the free version.
13060 *
13061 * @author Leo Fajardo (@leorw)
13062 * @since 2.3.2
13063 */
13064 ! is_object( $this->_get_license() )
13065 ) {
13066 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13067 return;
13068 }
13069
13070 // Add license activation link and AJAX request handler.
13071 if ( self::is_plugins_page() ) {
13072 $is_network_admin = fs_is_network_admin();
13073
13074 if (
13075 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13076 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13077 ) {
13078 /**
13079 * @since 1.2.0 Add license action link only on plugins page.
13080 */
13081 $this->_add_license_action_link();
13082 }
13083 }
13084
13085 // Add license activation AJAX callback.
13086 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13087
13088 // Add resend license AJAX callback.
13089 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13090 }
13091
13092 /**
13093 * Prepares page to include all required UI and logic for the "Change User" dialog.
13094 *
13095 * @author Leo Fajardo (@leorw)
13096 * @since 2.3.2
13097 */
13098 function _add_user_change_option() {
13099 if ( ! $this->should_handle_user_change() ) {
13100 return;
13101 }
13102
13103 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13104
13105 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13106 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13107 return;
13108 }
13109
13110 // Add user change AJAX handler.
13111 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13112 }
13113
13114 /**
13115 * @author Leo Fajardo (@leorw)
13116 * @since 2.3.2
13117 */
13118 function should_handle_user_change() {
13119 if ( ! $this->is_user_admin() ) {
13120 // Only admins can change user.
13121 return false;
13122 }
13123
13124 if ( $this->is_addon() ) {
13125 return false;
13126 }
13127
13128 if ( ! $this->is_registered() ) {
13129 return false;
13130 }
13131
13132 if (
13133 $this->is_network_active() &&
13134 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13135 ) {
13136 // Handle only on site-level "Account" section for now.
13137 return false;
13138 }
13139
13140 return true;
13141 }
13142
13143 /**
13144 * @author Leo Fajardo (@leorw)
13145 * @since 2.0.2
13146 */
13147 function _add_premium_version_upgrade_selection() {
13148 if ( ! $this->is_user_admin() ) {
13149 return;
13150 }
13151
13152 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13153 // This is relevant only to the free versions and premium versions without an active license.
13154 return;
13155 }
13156
13157 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13158 $this->_add_premium_version_upgrade_selection_action();
13159 }
13160 }
13161
13162 /**
13163 * @author Edgar Melkonyan
13164 * @since 2.4.1
13165 *
13166 * @throws Freemius_Exception
13167 */
13168 function _toggle_whitelabel_mode_ajax_handler() {
13169 $this->_logger->entrance();
13170
13171 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13172
13173 if ( ! $this->is_user_admin() ) {
13174 // Only for admins.
13175 self::shoot_ajax_failure();
13176 }
13177
13178 $license = $this->get_api_user_scope()->call(
13179 "/licenses/{$this->_site->license_id}.json",
13180 'put',
13181 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13182 );
13183
13184 if ( ! $this->is_api_result_entity( $license ) ) {
13185 self::shoot_ajax_failure(
13186 FS_Api::is_api_error_object( $license ) ?
13187 $license->error->message :
13188 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13189 );
13190 }
13191
13192 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13193 $this->_store_licenses();
13194
13195 $this->_sync_license();
13196
13197 if ( ! $license->is_whitelabeled ) {
13198 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13199 } else {
13200 $this->_admin_notices->add_sticky(
13201 sprintf(
13202 $this->get_text_inline(
13203 '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.',
13204 'license_whitelabeled'
13205 ),
13206 "<strong>{$this->get_plugin_title()}</strong>",
13207 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13208 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13209 ),
13210 'license_whitelabeled'
13211 );
13212 }
13213
13214 self::shoot_ajax_response( array( 'success' => true ) );
13215 }
13216
13217 /**
13218 * @author Leo Fajardo (@leorw)
13219 * @since 2.3.0
13220 */
13221 function _add_beta_mode_update_handler() {
13222 if ( ! $this->is_user_admin() ) {
13223 return;
13224 }
13225
13226 if ( ! $this->is_premium() ) {
13227 return;
13228 }
13229
13230 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13231 }
13232
13233 /**
13234 * @author Leo Fajardo (@leorw)
13235 * @since 2.3.0
13236 */
13237 function _set_beta_mode_ajax_handler() {
13238 $this->_logger->entrance();
13239
13240 $this->check_ajax_referer( 'set_beta_mode' );
13241
13242 if ( ! $this->is_user_admin() ) {
13243 // Only for admins.
13244 self::shoot_ajax_failure();
13245 }
13246
13247 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13248
13249 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13250 self::shoot_ajax_failure();
13251 }
13252
13253 $site = $this->get_api_site_scope()->call(
13254 '',
13255 'put',
13256 array(
13257 'is_beta' => ( 'true' == $is_beta ),
13258 'fields' => 'is_beta'
13259 )
13260 );
13261
13262 if ( ! $this->is_api_result_entity( $site ) ) {
13263 self::shoot_ajax_failure(
13264 FS_Api::is_api_error_object( $site ) ?
13265 $site->error->message :
13266 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13267 );
13268 }
13269
13270 $this->_site->is_beta = $site->is_beta;
13271 $this->_store_site();
13272
13273 self::shoot_ajax_response( array( 'success' => true ) );
13274 }
13275
13276 /**
13277 * License activation WP AJAX handler.
13278 *
13279 * @author Leo Fajardo (@leorw)
13280 * @since 1.1.9
13281 *
13282 * @uses Freemius::activate_license()
13283 */
13284 function _activate_license_ajax_action() {
13285 $this->_logger->entrance();
13286
13287 $this->check_ajax_referer( 'activate_license' );
13288
13289 $license_key = trim( fs_request_get( 'license_key' ) );
13290
13291 if ( empty( $license_key ) ) {
13292 exit;
13293 }
13294
13295 $sites = fs_is_network_admin() ?
13296 fs_request_get( 'sites', array(), 'post' ) :
13297 array();
13298
13299 $result = $this->activate_license(
13300 $license_key,
13301 $sites,
13302 fs_request_get_bool( 'is_marketing_allowed', null ),
13303 fs_request_get( 'blog_id', null ),
13304 fs_request_get( 'module_id', null, 'post' ),
13305 fs_request_get( 'user_id', null ),
13306 fs_request_get_bool( 'is_extensions_tracking_allowed', null )
13307 );
13308
13309 if (
13310 $result['success'] &&
13311 $this->is_bundle_license_auto_activation_enabled()
13312 ) {
13313 $license = new FS_Plugin_License();
13314 $license->secret_key = $license_key;
13315
13316 $this->maybe_activate_bundle_license( $license, $sites );
13317 }
13318
13319 echo json_encode( $result );
13320
13321 exit;
13322 }
13323
13324 /**
13325 * User change WP AJAX handler.
13326 *
13327 * @author Leo Fajardo (@leorw)
13328 * @since 2.3.2
13329 */
13330 function _user_change_ajax_action() {
13331 $this->_logger->entrance();
13332
13333 $this->check_ajax_referer( 'change_user' );
13334
13335 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13336 $new_user_id = fs_request_get( 'user_id' );
13337
13338 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13339 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13340 }
13341
13342 $params = array();
13343
13344 if ( ! empty( $new_email_address ) ) {
13345 $params['user_email'] = $new_email_address;
13346 } else {
13347 $params['user_id'] = $new_user_id;
13348 }
13349
13350 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13351 $install_ids = array();
13352
13353 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13354 $install_ids[ $slug ] = $install_info['install']->id;
13355 }
13356
13357 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13358
13359 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13360
13361 if ( FS_Api::is_api_error( $install ) ) {
13362 $error = '';
13363
13364 if ( is_object( $install ) ) {
13365 switch ( $install->error->code ) {
13366 case 'user_exist':
13367 $error = (
13368 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13369 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13370 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>' ) .
13371 sprintf(
13372 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13373 $this->get_account_url( 'change_owner', array(
13374 'state' => 'init',
13375 'candidate_email' => $new_email_address
13376 ) ),
13377 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13378 )
13379 );
13380 break;
13381 }
13382 }
13383
13384 if ( empty( $error ) ) {
13385 $error = FS_Api::is_api_error_object( $install ) ?
13386 $install->error->message :
13387 var_export( $install->error, true );
13388 }
13389
13390 self::shoot_ajax_failure( $error );
13391 } else {
13392 if (
13393 // If successful ownership change.
13394 $this->get_user()->id != $install->user_id ||
13395 ! empty( $new_email_address )
13396 ) {
13397 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13398 }
13399 }
13400
13401 self::shoot_ajax_success();
13402 }
13403
13404 /**
13405 * @author Leo Fajardo (@leorw)
13406 * @since 2.3.2.14
13407 */
13408 function starting_migration() {
13409 if ( ! empty( $this->_storage->license_migration ) ) {
13410 // Do not overwrite the data if already set.
13411 return;
13412 }
13413
13414 $this->_storage->license_migration = array(
13415 'is_migrating' => true,
13416 'start_timestamp' => time()
13417 );
13418 }
13419
13420 /**
13421 * @author Leo Fajardo (@leorw)
13422 * @since 2.3.2.14
13423 */
13424 function is_migration() {
13425 if ( $this->is_addon() ) {
13426 return $this->get_parent_instance()->is_migration();
13427 }
13428
13429 if ( empty( $this->_storage->license_migration ) ) {
13430 return false;
13431 }
13432
13433 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13434 return false;
13435 }
13436
13437 return (
13438 // Return `true` if the migration is within 5 minutes from the starting time.
13439 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13440 );
13441 }
13442
13443 /**
13444 *
13445 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13446 *
13447 * @author Vova Feldman (@svovaf)
13448 * @since 2.3.0
13449 *
13450 * @param string $license_key
13451 * @param null|bool $is_marketing_allowed
13452 * @param null|number $plugin_id
13453 * @param array $sites
13454 * @param int $blog_id
13455 *
13456 * @return array {
13457 * @var bool $success
13458 * @var string $error
13459 * @var string $next_page
13460 * }
13461 *
13462 * @uses Freemius::activate_license()
13463 */
13464 function activate_migrated_license(
13465 $license_key,
13466 $is_marketing_allowed = null,
13467 $plugin_id = null,
13468 $sites = array(),
13469 $blog_id = null
13470 ) {
13471 $this->_logger->entrance();
13472
13473 $result = $this->activate_license(
13474 $license_key,
13475 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13476 $this->get_sites_for_network_level_optin() :
13477 $sites,
13478 $is_marketing_allowed,
13479 $blog_id,
13480 $plugin_id
13481 );
13482
13483 // No need to show the sticky after license activation notice after migrating a license.
13484 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13485
13486 return $result;
13487 }
13488
13489 /**
13490 * @author Leo Fajardo (@leorw)
13491 * @since 2.3.1
13492 *
13493 * @return string
13494 */
13495 function get_pricing_js_path() {
13496 if ( ! isset( $this->_pricing_js_path ) ) {
13497 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
13498
13499 if ( empty( $pricing_js_path ) ) {
13500 global $fs_active_plugins;
13501
13502 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13503 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13504 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13505
13506 $pricing_js_path = $plugin_or_theme_root_dir
13507 . '/'
13508 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13509 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13510 . '/includes/freemius-pricing/freemius-pricing.js';
13511
13512 break;
13513 }
13514 }
13515 }
13516
13517 $this->_pricing_js_path = $pricing_js_path;
13518 }
13519
13520 return $this->_pricing_js_path;
13521 }
13522
13523 /**
13524 * @author Leo Fajardo (@leorw)
13525 * @since 2.3.1
13526 *
13527 * @return bool
13528 */
13529 function should_use_external_pricing() {
13530 if ( is_null( $this->_use_external_pricing ) ) {
13531 $pricing_js_path = $this->get_pricing_js_path();
13532
13533 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
13534 }
13535
13536 return $this->_use_external_pricing;
13537 }
13538
13539 /**
13540 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13541 *
13542 * @author Vova Feldman (@svovaf)
13543 * @since 2.2.4
13544 * @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).
13545 * @param string $license_key
13546 * @param array $sites
13547 * @param null|bool $is_marketing_allowed
13548 * @param null|int $blog_id
13549 * @param null|number $plugin_id
13550 * @param null|number $license_owner_id
13551 *
13552 * @return array {
13553 * @var bool $success
13554 * @var string $error
13555 * @var string $next_page
13556 * }
13557 */
13558 private function activate_license(
13559 $license_key,
13560 $sites = array(),
13561 $is_marketing_allowed = null,
13562 $blog_id = null,
13563 $plugin_id = null,
13564 $license_owner_id = null,
13565 $is_extensions_tracking_allowed = null
13566 ) {
13567 $this->_logger->entrance();
13568
13569 $license_key = trim( $license_key );
13570
13571 $is_network_activation_or_migration = (
13572 fs_is_network_admin() ||
13573 ( ! empty( $sites ) && $this->is_migration() )
13574 );
13575
13576 if ( ! $is_network_activation_or_migration ) {
13577 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13578 $sites = array();
13579 }
13580
13581 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13582 $this :
13583 $this->get_addon_instance( $plugin_id );
13584
13585 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
13586
13587 $error = false;
13588 $next_page = false;
13589
13590 $has_valid_blog_id = is_numeric( $blog_id );
13591
13592 $user = null;
13593
13594 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13595 /**
13596 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13597 *
13598 * @author Vova Feldman (@svovaf)
13599 */
13600 $user = $fs->get_parent_instance()->get_current_or_network_user();
13601 } else if ( $fs->is_registered() ) {
13602 $user = $fs->get_current_or_network_user();
13603 }
13604
13605 if ( $has_valid_blog_id ) {
13606 /**
13607 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13608 *
13609 * @author Leo Fajardo (@leorw)
13610 */
13611 $fs->switch_to_blog( $blog_id );
13612 }
13613
13614 if ( is_object( $user ) ) {
13615 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13616 // If no specific blog ID was provided, activate the license for all sites in the network.
13617 $blog_2_install_map = array();
13618 $site_ids = array();
13619
13620 foreach ( $sites as $site ) {
13621 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13622 continue;
13623 }
13624
13625 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13626
13627 if ( is_object( $install ) ) {
13628 $blog_2_install_map[ $site['blog_id'] ] = $install;
13629 } else {
13630 $site_ids[] = $site['blog_id'];
13631 }
13632 }
13633
13634 if ( ! empty( $blog_2_install_map ) ) {
13635 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13636
13637 if ( true !== $result ) {
13638 $error = FS_Api::is_api_error_object( $result ) ?
13639 $result->error->message :
13640 var_export( $result, true );
13641 }
13642 }
13643
13644 if ( empty( $error ) && ! empty( $site_ids ) ) {
13645 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13646
13647 if ( true !== $result ) {
13648 $error = FS_Api::is_api_error_object( $result ) ?
13649 $result->error->message :
13650 var_export( $result, true );
13651 }
13652 }
13653 } else {
13654 if ( $fs->is_registered() ) {
13655 $params = array(
13656 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13657 );
13658
13659 $install_ids = array();
13660
13661 $change_owner = FS_User::is_valid_id( $license_owner_id );
13662
13663 if ( $change_owner ) {
13664 $params['user_id'] = $license_owner_id;
13665
13666 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13667
13668 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13669 $install_ids[ $slug ] = $install_info['install']->id;
13670 }
13671
13672 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13673 }
13674
13675 $api = $fs->get_api_site_scope();
13676
13677 $install = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13678
13679 if ( FS_Api::is_api_error( $install ) ) {
13680 $error = FS_Api::is_api_error_object( $install ) ?
13681 $install->error->message :
13682 var_export( $install->error, true );
13683 } else {
13684 $fs->reconnect_locally( $has_valid_blog_id );
13685
13686 if (
13687 $change_owner &&
13688 // If successful ownership change.
13689 $fs->get_user()->id != $install->user_id
13690 ) {
13691 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13692 }
13693 }
13694 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13695 $result = $fs->activate_license_on_site( $user, $license_key );
13696
13697 if ( true !== $result ) {
13698 $error = FS_Api::is_api_error_object( $result ) ?
13699 $result->error->message :
13700 var_export( $result, true );
13701 }
13702 }
13703 }
13704
13705 if ( empty( $error ) ) {
13706 $fs->network_upgrade_mode_completed();
13707
13708 $fs->_user = $user;
13709
13710 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13711 $fs->_site = $fs->get_network_install();
13712 }
13713
13714 $fs->_sync_license( true, $has_valid_blog_id );
13715
13716 $this->maybe_sync_install_user();
13717
13718 $next_page = $fs->is_addon() ?
13719 $fs->get_parent_instance()->get_account_url() :
13720 $fs->get_after_activation_url( 'after_connect_url' );
13721 }
13722 } else {
13723 $next_page = $fs->opt_in(
13724 false,
13725 false,
13726 false,
13727 $license_key,
13728 false,
13729 false,
13730 false,
13731 $is_marketing_allowed,
13732 $sites
13733 );
13734
13735 if ( isset( $next_page->error ) ) {
13736 $error = $next_page->error;
13737 } else {
13738 if ( $is_network_activation_or_migration ) {
13739 /**
13740 * Get the list of sites that were just opted-in (and license activated).
13741 * This is an optimization for the next part below saving some DB queries.
13742 */
13743 $connected_sites = array();
13744 foreach ( $sites as $site ) {
13745 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13746 $connected_sites[ $site['blog_id'] ] = true;
13747 }
13748 }
13749
13750 $all_sites = self::get_sites();
13751 $pending_sites = array();
13752
13753 /**
13754 * 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.
13755 *
13756 * @author Vova Feldman (@svovaf)
13757 */
13758 foreach ( $all_sites as $site ) {
13759 $blog_id = self::get_site_blog_id( $site );
13760
13761 if ( isset( $connected_sites[ $blog_id ] ) ) {
13762 // Site was just connected.
13763 continue;
13764 }
13765
13766 if ( $fs->is_installed_on_site( $blog_id ) ) {
13767 // Site was already connected before.
13768 continue;
13769 }
13770
13771 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13772 // Site's connection was delegated.
13773 continue;
13774 }
13775
13776 if ( $fs->is_anonymous_site( $blog_id ) ) {
13777 // Site connection was already skipped.
13778 continue;
13779 }
13780
13781 $pending_sites[] = self::get_site_info( $site );
13782 }
13783
13784 if ( ! empty( $pending_sites ) ) {
13785 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13786 $fs->skip_connection( $pending_sites );
13787 } else {
13788 $fs->delegate_connection( $pending_sites );
13789 }
13790 }
13791 }
13792 }
13793 }
13794
13795 if ( false === $error && true === $fs->_storage->require_license_activation ) {
13796 $fs->_storage->require_license_activation = false;
13797 }
13798
13799 $result = array(
13800 'success' => ( false === $error )
13801 );
13802
13803 if ( false !== $error ) {
13804 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
13805 } else {
13806 if ( $fs->is_addon() || $fs->has_addons() ) {
13807 /**
13808 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
13809 * an updated valid user licenses collection will be fetched from the server which is used to also
13810 * update the account add-ons (add-ons the user has licenses for).
13811 *
13812 * @author Leo Fajardo (@leorw)
13813 * @since 2.2.4
13814 */
13815 $fs->purge_valid_user_licenses_cache();
13816 }
13817
13818 $result['next_page'] = $next_page;
13819 }
13820
13821 return $result;
13822 }
13823
13824 /**
13825 * @author Leo Fajardo (@leorw)
13826 * @since 2.3.2
13827 *
13828 * @return array {
13829 * @key string Product slug.
13830 * @value array {
13831 * @property FS_Site $site
13832 * @property FS_Plugin_License $license
13833 * }
13834 * }
13835 */
13836 private function get_parent_and_addons_installs_info() {
13837 $fs = $this->is_addon() ?
13838 $this->get_parent_instance() :
13839 $this;
13840
13841 $installed_addons_ids = array();
13842
13843 $installed_addons_instances = $fs->get_installed_addons();
13844 foreach ( $installed_addons_instances as $instance ) {
13845 $installed_addons_ids[] = $instance->get_id();
13846 }
13847
13848 $addons_ids = array_unique( array_merge(
13849 $installed_addons_ids,
13850 $fs->get_updated_account_addons()
13851 ) );
13852
13853 // Add parent product info.
13854 $installs_info_by_slug_map = array(
13855 $fs->get_slug() => array(
13856 'install' => $fs->get_site(),
13857 'license' => $fs->_get_license()
13858 )
13859 );
13860
13861 foreach ( $addons_ids as $addon_id ) {
13862 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
13863
13864 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
13865
13866 if ( ! $addon_info['is_connected'] ) {
13867 // Add-on is not associated with an install entity.
13868 continue;
13869 }
13870
13871 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
13872 'install' => $addon_info['site'],
13873 'license' => isset( $addon_info['license'] ) ?
13874 $addon_info['license'] :
13875 null
13876 );
13877 }
13878
13879 return $installs_info_by_slug_map;
13880 }
13881
13882 /**
13883 * @author Leo Fajardo (@leorw)
13884 * @since 1.2.3.1
13885 */
13886 function _network_activate_ajax_action() {
13887 $this->_logger->entrance();
13888
13889 $this->check_ajax_referer( 'network_activate' );
13890
13891 $plugin_id = fs_request_get( 'module_id', '', 'post' );
13892 $fs = ( $plugin_id == $this->_module_id ) ?
13893 $this :
13894 $this->get_addon_instance( $plugin_id );
13895
13896 $error = false;
13897
13898 $sites = fs_request_get( 'sites', array(), 'post' );
13899 if ( is_array( $sites ) && ! empty( $sites ) ) {
13900 $sites_by_action = array(
13901 'allow' => array(),
13902 'delegate' => array(),
13903 'skip' => array()
13904 );
13905
13906 foreach ( $sites as $site ) {
13907 $sites_by_action[ $site['action'] ][] = $site;
13908 }
13909
13910 $total_sites = count( $sites );
13911 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
13912
13913 $next_page = '';
13914
13915 $has_any_install = fs_request_get_bool( 'has_any_install' );
13916
13917 if ( $total_sites === $total_sites_to_delegate &&
13918 ! $this->is_network_upgrade_mode() &&
13919 ! $has_any_install
13920 ) {
13921 $this->delegate_connection();
13922 } else {
13923 if ( ! empty( $sites_by_action['delegate'] ) ) {
13924 $this->delegate_connection( $sites_by_action['delegate'] );
13925 }
13926
13927 if ( ! empty( $sites_by_action['skip'] ) ) {
13928 $this->skip_connection( $sites_by_action['skip'] );
13929 }
13930
13931 if ( empty( $sites_by_action['allow'] ) ) {
13932 if ( $has_any_install ) {
13933 $first_install = $fs->find_first_install();
13934
13935 if ( ! is_null( $first_install ) ) {
13936 $fs->_site = $first_install['install'];
13937 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
13938
13939 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
13940 $fs->_storage->network_user_id = $fs->_user->id;
13941 }
13942 }
13943 } else {
13944 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
13945 $next_page = $fs->opt_in(
13946 false,
13947 false,
13948 false,
13949 false,
13950 false,
13951 false,
13952 false,
13953 fs_request_get_bool( 'is_marketing_allowed', null ),
13954 $sites_by_action['allow']
13955 );
13956 } else {
13957 $next_page = $fs->install_with_user(
13958 $this->get_network_user(),
13959 false,
13960 false,
13961 false,
13962 true,
13963 $sites_by_action['allow']
13964 );
13965 }
13966
13967 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
13968 $error = $next_page->error;
13969 }
13970 }
13971 }
13972
13973 if ( empty( $next_page ) ) {
13974 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
13975 }
13976 } else {
13977 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
13978 }
13979
13980 $result = array(
13981 'success' => ( false === $error )
13982 );
13983
13984 if ( false !== $error ) {
13985 $result['error'] = $error;
13986 } else {
13987 $result['next_page'] = $next_page;
13988 }
13989
13990 echo json_encode( $result );
13991
13992 exit;
13993 }
13994
13995 /**
13996 * Billing update AJAX callback.
13997 *
13998 * @author Vova Feldman (@svovaf)
13999 * @since 1.2.1.5
14000 */
14001 function _update_billing_ajax_action() {
14002 $this->_logger->entrance();
14003
14004 $this->check_ajax_referer( 'update_billing' );
14005
14006 if ( ! $this->is_user_admin() ) {
14007 // Only for admins.
14008 self::shoot_ajax_failure();
14009 }
14010
14011 $billing = fs_request_get( 'billing' );
14012
14013 $api = $this->get_api_user_scope();
14014 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14015 'plugin_id' => $this->get_parent_id(),
14016 ) ) );
14017
14018 if ( ! $this->is_api_result_entity( $result ) ) {
14019 self::shoot_ajax_failure();
14020 }
14021
14022 // Purge cached billing.
14023 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14024
14025 self::shoot_ajax_success();
14026 }
14027
14028 /**
14029 * Trial start for anonymous users (AJAX callback).
14030 *
14031 * @author Vova Feldman (@svovaf)
14032 * @since 1.2.1.5
14033 */
14034 function _start_trial_ajax_action() {
14035 $this->_logger->entrance();
14036
14037 $this->check_ajax_referer( 'start_trial' );
14038
14039 if ( ! $this->is_user_admin() ) {
14040 // Only for admins.
14041 self::shoot_ajax_failure();
14042 }
14043
14044 $trial_data = fs_request_get( 'trial' );
14045
14046 $next_page = $this->opt_in(
14047 false,
14048 false,
14049 false,
14050 false,
14051 false,
14052 $trial_data['plan_id']
14053 );
14054
14055 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14056 self::shoot_ajax_failure(
14057 isset( $next_page->error ) ?
14058 $next_page->error->message :
14059 var_export( $next_page, true )
14060 );
14061 }
14062
14063 $this->shoot_ajax_success( array(
14064 'next_page' => $next_page,
14065 ) );
14066 }
14067
14068 /**
14069 * @author Leo Fajardo (@leorw)
14070 * @since 1.2.0
14071 */
14072 function _resend_license_key_ajax_action() {
14073 $this->_logger->entrance();
14074
14075 $this->check_ajax_referer( 'resend_license_key' );
14076
14077 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14078
14079 if ( empty( $email_address ) ) {
14080 exit;
14081 }
14082
14083 $error = false;
14084
14085 $api = $this->get_api_plugin_scope();
14086 $result = $api->call( '/licenses/resend.json', 'post',
14087 array(
14088 'email' => $email_address,
14089 'url' => home_url(),
14090 )
14091 );
14092
14093 if ( is_object( $result ) && isset( $result->error ) ) {
14094 $error = $result->error;
14095
14096 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14097 $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' );
14098 } else if ( 'no_license' === $error->code ) {
14099 $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' );
14100 } else {
14101 $error = $error->message;
14102 }
14103 }
14104
14105 $licenses = array(
14106 'success' => ( false === $error )
14107 );
14108
14109 if ( false !== $error ) {
14110 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14111 }
14112
14113 echo json_encode( $licenses );
14114
14115 exit;
14116 }
14117
14118 /**
14119 * @author Vova Feldman (@svovaf)
14120 * @since 1.2.1.8
14121 *
14122 * @var string
14123 */
14124 private static $_pagenow;
14125
14126 /**
14127 * Get current page or the referer if executing a WP AJAX request.
14128 *
14129 * @author Vova Feldman (@svovaf)
14130 * @since 1.2.1.8
14131 *
14132 * @return string
14133 */
14134 static function get_current_page() {
14135 if ( ! isset( self::$_pagenow ) ) {
14136 global $pagenow;
14137 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14138 /**
14139 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14140 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14141 *
14142 * @author Leo Fajardo (@leorw)
14143 * @since 2.2.3
14144 */
14145 if ( is_network_admin() ) {
14146 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14147 } else if ( is_user_admin() ) {
14148 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14149 } else {
14150 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14151 }
14152
14153 $pagenow = $self_matches[1];
14154 $pagenow = trim( $pagenow, '/' );
14155 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14156 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14157 $pagenow = 'index.php';
14158 } else {
14159 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14160 $pagenow = strtolower( $self_matches[1] );
14161 if ( '.php' !== substr($pagenow, -4, 4) )
14162 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14163 }
14164 }
14165
14166 self::$_pagenow = $pagenow;
14167
14168 if ( self::is_ajax() &&
14169 'admin-ajax.php' === $pagenow
14170 ) {
14171 $referer = fs_get_raw_referer();
14172
14173 if ( is_string( $referer ) ) {
14174 $parts = explode( '?', $referer );
14175
14176 self::$_pagenow = basename( $parts[0] );
14177 }
14178 }
14179 }
14180
14181 return self::$_pagenow;
14182 }
14183
14184 /**
14185 * Helper method to check if user in the plugins page.
14186 *
14187 * @author Vova Feldman (@svovaf)
14188 * @since 1.2.1.5
14189 *
14190 * @return bool
14191 */
14192 static function is_plugins_page() {
14193 return ( 'plugins.php' === self::get_current_page() );
14194 }
14195
14196 /**
14197 * @author Leo Fajardo (@leorw)
14198 * @since 2.2.3
14199 *
14200 * @return bool
14201 */
14202 static function is_plugin_install_page() {
14203 return ( 'plugin-install.php' === self::get_current_page() );
14204 }
14205
14206 /**
14207 * @author Leo Fajardo (@leorw)
14208 * @since 2.0.2
14209 *
14210 * @return bool
14211 */
14212 static function is_updates_page() {
14213 return ( 'update-core.php' === self::get_current_page() );
14214 }
14215
14216 /**
14217 * Helper method to check if user in the themes page.
14218 *
14219 * @author Vova Feldman (@svovaf)
14220 * @since 1.2.2.6
14221 *
14222 * @return bool
14223 */
14224 static function is_themes_page() {
14225 return ( 'themes.php' === self::get_current_page() );
14226 }
14227
14228 #----------------------------------------------------------------------------------
14229 #region Affiliation
14230 #----------------------------------------------------------------------------------
14231
14232 /**
14233 * @author Leo Fajardo (@leorw)
14234 * @since 1.2.3
14235 *
14236 * @return bool
14237 */
14238 function has_affiliate_program() {
14239 if ( ! is_object( $this->_plugin ) ) {
14240 return false;
14241 }
14242
14243 return $this->_plugin->has_affiliate_program();
14244 }
14245
14246 /**
14247 * @author Leo Fajardo (@leorw)
14248 * @since 1.2.4
14249 */
14250 private function fetch_affiliate_terms() {
14251 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14252 $plugins_api = $this->get_api_plugin_scope();
14253 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14254
14255 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14256 return;
14257 }
14258
14259 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14260 }
14261 }
14262
14263 /**
14264 * @author Leo Fajardo (@leorw)
14265 * @since 1.2.4
14266 */
14267 private function fetch_affiliate_and_custom_terms() {
14268 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14269 $application_data = $this->_storage->affiliate_application_data;
14270 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14271
14272 $users_api = $this->get_api_user_scope();
14273 $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14274 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14275 if ( ! empty( $result->affiliates ) ) {
14276 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14277
14278 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14279 $application_data['status'] = $affiliate->status;
14280 $this->_storage->affiliate_application_data = $application_data;
14281 }
14282
14283 if ( $affiliate->is_using_custom_terms ) {
14284 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14285 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14286 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14287 }
14288 }
14289
14290 $this->affiliate = $affiliate;
14291 }
14292 }
14293 }
14294 }
14295
14296 /**
14297 * @author Leo Fajardo (@leorw)
14298 * @since 1.2.3
14299 */
14300 private function fetch_affiliate_and_terms() {
14301 $this->_logger->entrance();
14302
14303 $this->fetch_affiliate_terms();
14304 $this->fetch_affiliate_and_custom_terms();
14305 }
14306
14307 /**
14308 * @author Leo Fajardo (@leorw)
14309 * @since 1.2.3
14310 *
14311 * @return FS_Affiliate
14312 */
14313 function get_affiliate() {
14314 return $this->affiliate;
14315 }
14316
14317
14318 /**
14319 * @author Leo Fajardo (@leorw)
14320 * @since 1.2.3
14321 *
14322 * @return FS_AffiliateTerms
14323 */
14324 function get_affiliate_terms() {
14325 return is_object( $this->custom_affiliate_terms ) ?
14326 $this->custom_affiliate_terms :
14327 $this->plugin_affiliate_terms;
14328 }
14329
14330 /**
14331 * @author Leo Fajardo (@leorw)
14332 * @since 1.2.3
14333 */
14334 function _submit_affiliate_application() {
14335 $this->_logger->entrance();
14336
14337 $this->check_ajax_referer( 'submit_affiliate_application' );
14338
14339 if ( ! $this->is_user_admin() ) {
14340 // Only for admins.
14341 self::shoot_ajax_failure();
14342 }
14343
14344 $affiliate = fs_request_get( 'affiliate' );
14345
14346 if ( empty( $affiliate['promotion_methods'] ) ) {
14347 unset( $affiliate['promotion_methods'] );
14348 }
14349
14350 if ( ! empty( $affiliate['additional_domains'] ) ) {
14351 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14352 }
14353
14354 if ( ! $this->is_registered() ) {
14355 // Opt in but don't track usage.
14356 $next_page = $this->opt_in(
14357 false,
14358 false,
14359 false,
14360 false,
14361 false,
14362 false,
14363 true
14364 );
14365
14366 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14367 self::shoot_ajax_failure(
14368 isset( $next_page->error ) ?
14369 $next_page->error->message :
14370 var_export( $next_page, true )
14371 );
14372 } else if ( $this->is_pending_activation() ) {
14373 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) );
14374 }
14375 }
14376
14377 $this->fetch_affiliate_terms();
14378
14379 $api = $this->get_api_user_scope();
14380 $result = $api->call(
14381 ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14382 'post',
14383 $affiliate
14384 );
14385
14386 if ( $this->is_api_error( $result ) ) {
14387 self::shoot_ajax_failure(
14388 isset( $result->error ) ?
14389 $result->error->message :
14390 var_export( $result, true )
14391 );
14392 } else {
14393 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14394 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14395 }
14396
14397 $affiliate_application_data = array(
14398 'status' => 'pending',
14399 'stats_description' => $affiliate['stats_description'],
14400 'promotion_method_description' => $affiliate['promotion_method_description'],
14401 );
14402
14403 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14404 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14405 }
14406
14407 if ( ! empty( $affiliate['domain'] ) ) {
14408 $affiliate_application_data['domain'] = $affiliate['domain'];
14409 }
14410
14411 if ( ! empty( $affiliate['additional_domains'] ) ) {
14412 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14413 }
14414
14415 $this->_storage->affiliate_application_data = $affiliate_application_data;
14416 }
14417
14418 // Purge cached affiliate.
14419 $api->purge_cache( 'affiliate.json' );
14420
14421 self::shoot_ajax_success( $result );
14422 }
14423
14424 /**
14425 * @author Leo Fajardo (@leorw)
14426 * @since 1.2.3
14427 *
14428 * @return array|null
14429 */
14430 function get_affiliate_application_data() {
14431 if ( empty( $this->_storage->affiliate_application_data ) ) {
14432 return null;
14433 }
14434
14435 return $this->_storage->affiliate_application_data;
14436 }
14437
14438 #endregion Affiliation ------------------------------------------------------------
14439
14440 #----------------------------------------------------------------------------------
14441 #region URL Generators
14442 #----------------------------------------------------------------------------------
14443
14444 /**
14445 * Alias to pricing_url().
14446 *
14447 * @author Vova Feldman (@svovaf)
14448 * @since 1.0.2
14449 *
14450 * @uses pricing_url()
14451 *
14452 * @param string $period Billing cycle
14453 * @param bool $is_trial
14454 *
14455 * @return string
14456 */
14457 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14458 return $this->pricing_url( $period, $is_trial );
14459 }
14460
14461 /**
14462 * @author Vova Feldman (@svovaf)
14463 * @since 1.0.9
14464 *
14465 * @uses get_upgrade_url()
14466 *
14467 * @return string
14468 */
14469 function get_trial_url() {
14470 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14471 }
14472
14473 /**
14474 * @author Leo Fajardo (@leorw)
14475 * @since 2.1.4
14476 *
14477 * @param string $new_version
14478 *
14479 * @return string
14480 */
14481 function version_upgrade_checkout_link( $new_version ) {
14482 if ( ! is_object( $this->_license ) ) {
14483 $url = $this->pricing_url();
14484
14485 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14486 } else {
14487 $subscription = $this->_get_subscription( $this->_license->id );
14488
14489 $url = $this->checkout_url(
14490 is_object( $subscription ) ?
14491 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14492 WP_FS__PERIOD_LIFETIME,
14493 false,
14494 array( 'licenses' => $this->_license->quota )
14495 );
14496
14497 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14498 }
14499
14500 return sprintf(
14501 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14502 sprintf( '<a href="%s">%s</a>', $url, $purchase_license_text ),
14503 $new_version
14504 );
14505 }
14506
14507 /**
14508 * Plugin's pricing URL.
14509 *
14510 * @author Vova Feldman (@svovaf)
14511 * @since 1.0.4
14512 *
14513 * @param string $billing_cycle Billing cycle
14514 *
14515 * @param bool $is_trial
14516 *
14517 * @return string
14518 */
14519 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14520 $this->_logger->entrance();
14521
14522 $params = array(
14523 'billing_cycle' => $billing_cycle
14524 );
14525
14526 if ( $is_trial ) {
14527 $params['trial'] = 'true';
14528 }
14529
14530 $url = $this->is_addon() ?
14531 $this->_parent->addon_url( $this->_slug ) :
14532 $this->_get_admin_page_url( 'pricing', $params );
14533
14534 return $this->apply_filters( 'pricing_url', $url );
14535 }
14536
14537 /**
14538 * Checkout page URL.
14539 *
14540 * @author Vova Feldman (@svovaf)
14541 * @since 1.0.6
14542 *
14543 * @param string $billing_cycle Billing cycle
14544 * @param bool $is_trial
14545 * @param array $extra (optional) Extra parameters, override other query params.
14546 * @param bool|null $network
14547 *
14548 * @return string
14549 */
14550 function checkout_url(
14551 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14552 $is_trial = false,
14553 $extra = array(),
14554 $network = null
14555 ) {
14556 $this->_logger->entrance();
14557
14558 $params = array(
14559 'checkout' => 'true',
14560 'billing_cycle' => $billing_cycle,
14561 );
14562
14563 if ( $is_trial ) {
14564 $params['trial'] = 'true';
14565 }
14566
14567 /**
14568 * Params in extra override other params.
14569 */
14570 $params = array_merge( $params, $extra );
14571
14572 return $this->_get_admin_page_url( 'pricing', $params, $network );
14573 }
14574
14575 /**
14576 * Add-on checkout URL.
14577 *
14578 * @author Vova Feldman (@svovaf)
14579 * @since 1.1.7
14580 *
14581 * @param number $addon_id
14582 * @param number $pricing_id
14583 * @param string $billing_cycle
14584 * @param bool $is_trial
14585 * @param bool|null $network
14586 *
14587 * @return string
14588 */
14589 function addon_checkout_url(
14590 $addon_id,
14591 $pricing_id,
14592 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14593 $is_trial = false,
14594 $network = null
14595 ) {
14596 return $this->checkout_url( $billing_cycle, $is_trial, array(
14597 'plugin_id' => $addon_id,
14598 'pricing_id' => $pricing_id,
14599 ), $network );
14600 }
14601
14602 #endregion
14603
14604 #endregion ------------------------------------------------------------------
14605
14606 /**
14607 * Check if plugin has any add-ons.
14608 *
14609 * @author Vova Feldman (@svovaf)
14610 * @since 1.0.5
14611 *
14612 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14613 *
14614 * @return bool
14615 */
14616 function has_addons() {
14617 $this->_logger->entrance();
14618
14619 return $this->_has_addons;
14620 }
14621
14622 /**
14623 * Check if plugin can work in anonymous mode.
14624 *
14625 * @author Vova Feldman (@svovaf)
14626 * @since 1.0.9
14627 *
14628 * @return bool
14629 *
14630 * @deprecated Please use is_enable_anonymous() instead.
14631 */
14632 function enable_anonymous() {
14633 return $this->_enable_anonymous;
14634 }
14635
14636 /**
14637 * Check if plugin can work in anonymous mode.
14638 *
14639 * @author Vova Feldman (@svovaf)
14640 * @since 1.1.9
14641 *
14642 * @return bool
14643 */
14644 function is_enable_anonymous() {
14645 return $this->_enable_anonymous;
14646 }
14647
14648 /**
14649 * Check if plugin is premium only (no free plans).
14650 *
14651 * @author Vova Feldman (@svovaf)
14652 * @since 1.1.9
14653 *
14654 * @return bool
14655 */
14656 function is_only_premium() {
14657 return $this->_is_premium_only;
14658 }
14659
14660 /**
14661 * Checks if the plugin's type is "plugin". The other type is "theme".
14662 *
14663 * @author Leo Fajardo (@leorw)
14664 * @since 1.2.2
14665 *
14666 * @return bool
14667 */
14668 function is_plugin() {
14669 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14670 }
14671
14672 /**
14673 * @author Leo Fajardo (@leorw)
14674 * @since 1.2.2
14675 *
14676 * @return string
14677 */
14678 function get_module_type() {
14679 if ( ! isset( $this->_module_type ) ) {
14680 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14681 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14682 }
14683
14684 return $this->_module_type;
14685 }
14686
14687 /**
14688 * @author Leo Fajardo (@leorw)
14689 * @since 1.2.2
14690 *
14691 * @return string
14692 */
14693 function get_plugin_main_file_path() {
14694 return $this->_plugin_main_file_path;
14695 }
14696
14697 /**
14698 * Check if module has a premium code version.
14699 *
14700 * Serviceware module might be freemium without any
14701 * premium code version, where the paid features
14702 * are all part of the service.
14703 *
14704 * @author Vova Feldman (@svovaf)
14705 * @since 1.2.1.6
14706 *
14707 * @return bool
14708 */
14709 function has_premium_version() {
14710 return $this->_has_premium_version;
14711 }
14712
14713 /**
14714 * Check if feature supported with current site's plan.
14715 *
14716 * @author Vova Feldman (@svovaf)
14717 * @since 1.0.1
14718 *
14719 * @todo IMPLEMENT
14720 *
14721 * @param number $feature_id
14722 *
14723 * @throws Exception
14724 */
14725 function is_feature_supported( $feature_id ) {
14726 throw new Exception( 'not implemented' );
14727 }
14728
14729 /**
14730 * @author Vova Feldman (@svovaf)
14731 * @since 1.0.1
14732 *
14733 * @return bool Is running in SSL/HTTPS
14734 */
14735 function is_ssl() {
14736 return WP_FS__IS_HTTPS;
14737 }
14738
14739 /**
14740 * @author Vova Feldman (@svovaf)
14741 * @since 1.0.9
14742 *
14743 * @return bool Is running in AJAX call.
14744 *
14745 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
14746 */
14747 static function is_ajax() {
14748 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
14749 }
14750
14751 /**
14752 * Check if it's an AJAX call targeted for the current module.
14753 *
14754 * @author Vova Feldman (@svovaf)
14755 * @since 1.2.0
14756 *
14757 * @param array|string $actions Collection of AJAX actions.
14758 *
14759 * @return bool
14760 */
14761 function is_ajax_action( $actions ) {
14762 // Verify it's an ajax call.
14763 if ( ! self::is_ajax() ) {
14764 return false;
14765 }
14766
14767 // Verify the call is relevant for the plugin.
14768 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
14769 return false;
14770 }
14771
14772 // Verify it's one of the specified actions.
14773 if ( is_string( $actions ) ) {
14774 $actions = explode( ',', $actions );
14775 }
14776
14777 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14778 $ajax_action = fs_request_get( 'action' );
14779
14780 foreach ( $actions as $action ) {
14781 if ( $ajax_action === $this->get_action_tag( $action ) ) {
14782 return true;
14783 }
14784 }
14785 }
14786
14787 return false;
14788 }
14789
14790 /**
14791 * Check if it's an AJAX call targeted for current request.
14792 *
14793 * @author Vova Feldman (@svovaf)
14794 * @since 1.2.0
14795 *
14796 * @param array|string $actions Collection of AJAX actions.
14797 * @param number|null $module_id
14798 *
14799 * @return bool
14800 */
14801 static function is_ajax_action_static( $actions, $module_id = null ) {
14802 // Verify it's an ajax call.
14803 if ( ! self::is_ajax() ) {
14804 return false;
14805 }
14806
14807
14808 if ( ! empty( $module_id ) ) {
14809 // Verify the call is relevant for the plugin.
14810 if ( $module_id != fs_request_get( 'module_id' ) ) {
14811 return false;
14812 }
14813 }
14814
14815 // Verify it's one of the specified actions.
14816 if ( is_string( $actions ) ) {
14817 $actions = explode( ',', $actions );
14818 }
14819
14820 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14821 $ajax_action = fs_request_get( 'action' );
14822
14823 foreach ( $actions as $action ) {
14824 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
14825 return true;
14826 }
14827 }
14828 }
14829
14830 return false;
14831 }
14832
14833 /**
14834 * @author Vova Feldman (@svovaf)
14835 * @since 1.1.7
14836 *
14837 * @return bool
14838 */
14839 static function is_cron() {
14840 return ( defined( 'DOING_CRON' ) && DOING_CRON );
14841 }
14842
14843 /**
14844 * Check if a real user is visiting the admin dashboard.
14845 *
14846 * @author Vova Feldman (@svovaf)
14847 * @since 1.1.7
14848 *
14849 * @return bool
14850 */
14851 function is_user_in_admin() {
14852 return (
14853 is_admin() &&
14854 ! self::is_ajax() &&
14855 ! self::is_cron() &&
14856 ( 'admin-post.php' !== self::get_current_page() )
14857 );
14858 }
14859
14860 /**
14861 * Check if a real user is in the customizer view.
14862 *
14863 * @author Vova Feldman (@svovaf)
14864 * @since 1.2.2.7
14865 *
14866 * @return bool
14867 */
14868 static function is_customizer() {
14869 return is_customize_preview();
14870 }
14871
14872 /**
14873 * Check if running in HTTPS and if site's plan matching the specified plan.
14874 *
14875 * @param string $plan
14876 * @param bool $exact
14877 *
14878 * @return bool
14879 */
14880 function is_ssl_and_plan( $plan, $exact = false ) {
14881 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
14882 }
14883
14884 /**
14885 * Construct plugin's settings page URL.
14886 *
14887 * @author Vova Feldman (@svovaf)
14888 * @since 1.0.4
14889 *
14890 * @param string $page
14891 * @param array $params
14892 * @param bool|null $network
14893 *
14894 * @return string
14895 */
14896 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
14897 if ( is_null( $network ) ) {
14898 $network = (
14899 $this->_is_network_active &&
14900 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
14901 );
14902 }
14903
14904 if ( 0 < count( $params ) ) {
14905 foreach ( $params as $k => $v ) {
14906 $params[ $k ] = urlencode( $v );
14907 }
14908 }
14909
14910 $page_param = $this->_menu->get_slug( $page );
14911
14912 if ( empty( $page ) &&
14913 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
14914 $this->show_opt_in_on_themes_page()
14915 ) {
14916 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
14917
14918 return add_query_arg(
14919 $params,
14920 $this->admin_url( 'themes.php', 'admin', $network )
14921 );
14922 }
14923
14924 if ( ! $this->has_settings_menu() ) {
14925 if ( ! empty( $page ) ) {
14926 // Module doesn't have a setting page, but since the request is for
14927 // a specific Freemius page, use the admin.php path.
14928 return add_query_arg( array_merge( $params, array(
14929 'page' => $page_param,
14930 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14931 } else {
14932 if ( $this->is_activation_mode() ) {
14933 /**
14934 * @author Vova Feldman
14935 * @since 1.2.1.6
14936 *
14937 * If plugin doesn't have a settings page, create one for the opt-in screen.
14938 */
14939 return add_query_arg( array_merge( $params, array(
14940 'page' => $this->_slug,
14941 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14942 } else {
14943 // Plugin without a settings page.
14944 return add_query_arg(
14945 $params,
14946 $this->admin_url( 'plugins.php', 'admin', $network )
14947 );
14948 }
14949 }
14950 }
14951
14952 // Module has a submenu settings page.
14953 if ( ! $this->_menu->is_top_level() ) {
14954 $parent_slug = $this->_menu->get_parent_slug();
14955 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
14956 $parent_slug :
14957 'admin.php';
14958
14959 return add_query_arg( array_merge( $params, array(
14960 'page' => $page_param,
14961 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
14962 }
14963
14964 // Module has a top level CPT settings page.
14965 if ( $this->_menu->is_cpt() ) {
14966 if ( empty( $page ) && $this->is_activation_mode() ) {
14967 return add_query_arg( array_merge( $params, array(
14968 'page' => $page_param
14969 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14970 } else {
14971 if ( ! empty( $page ) ) {
14972 $params['page'] = $page_param;
14973 }
14974
14975 return add_query_arg(
14976 $params,
14977 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
14978 );
14979 }
14980 }
14981
14982 // Module has a custom top level settings page.
14983 return add_query_arg( array_merge( $params, array(
14984 'page' => $page_param,
14985 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14986 }
14987
14988 #--------------------------------------------------------------------------------
14989 #region Multisite
14990 #--------------------------------------------------------------------------------
14991
14992 /**
14993 * @author Leo Fajardo (@leorw)
14994 * @since 2.0.0
14995 *
14996 * @return bool
14997 */
14998 function is_network_active() {
14999 return $this->_is_network_active;
15000 }
15001
15002 /**
15003 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15004 *
15005 * @author Leo Fajardo (@leorw)
15006 * @since 2.0.0
15007 *
15008 * @param array|null $sites
15009 */
15010 private function delegate_connection( $sites = null ) {
15011 $this->_logger->entrance();
15012
15013 $this->_admin_notices->remove_sticky( 'connect_account' );
15014
15015 if ( is_null( $sites ) ) {
15016 // All sites delegation.
15017 $this->_storage->store( 'is_delegated_connection', true, true, true );
15018 } else {
15019 // Specified sites delegation.
15020 foreach ( $sites as $site ) {
15021 $this->delegate_site_connection( $site['blog_id'] );
15022 }
15023 }
15024
15025 $this->network_upgrade_mode_completed();
15026 }
15027
15028 /**
15029 * Delegate specific network site conncetion to the site admin.
15030 *
15031 * @author Vova Feldman (@svovaf)
15032 * @since 2.0.0
15033 *
15034 * @param int $blog_id
15035 */
15036 private function delegate_site_connection( $blog_id ) {
15037 $this->_storage->store( 'is_delegated_connection', true, $blog_id, true );
15038 }
15039
15040 /**
15041 * Check if super-admin delegated the connection of ALL sites to the site admins.
15042 *
15043 * @author Vova Feldman (@svovaf)
15044 * @since 2.0.0
15045 *
15046 * @return bool
15047 */
15048 function is_network_delegated_connection() {
15049 if ( ! $this->_is_network_active ) {
15050 return false;
15051 }
15052
15053 return $this->_storage->get( 'is_delegated_connection', false, true );
15054 }
15055
15056 /**
15057 * @author Leo Fajardo (@leorw)
15058 * @since 2.0.0
15059 *
15060 * @param int $blog_id
15061 *
15062 * @return bool
15063 */
15064 function is_site_delegated_connection( $blog_id = 0 ) {
15065 if ( ! $this->_is_network_active ) {
15066 return false;
15067 }
15068
15069 if ( 0 == $blog_id ) {
15070 $blog_id = get_current_blog_id();
15071 }
15072
15073 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15074 }
15075
15076 /**
15077 * Check if delegated the connection. When running within the the network admin,
15078 * and haven't specified the blog ID, checks if network level delegated. If running
15079 * within a site admin or specified a blog ID, check if delegated the connection for
15080 * the current context site.
15081 *
15082 * If executed outside the the admin, check if delegated the connection
15083 * for the current context site OR the whole network.
15084 *
15085 * @author Vova Feldman (@svovaf)
15086 * @since 2.0.0
15087 *
15088 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15089 *
15090 * @return bool
15091 */
15092 function is_delegated_connection( $blog_id = 0 ) {
15093 if ( ! $this->_is_network_active ) {
15094 return false;
15095 }
15096
15097 if ( fs_is_network_admin() && 0 == $blog_id ) {
15098 return $this->is_network_delegated_connection();
15099 }
15100
15101 return (
15102 $this->is_network_delegated_connection() ||
15103 $this->is_site_delegated_connection( $blog_id )
15104 );
15105 }
15106
15107 /**
15108 * Check if the current module is active for the site.
15109 *
15110 * @author Vova Feldman (@svovaf)
15111 * @since 2.0.0
15112 *
15113 * @param int $blog_id
15114 *
15115 * @return bool
15116 */
15117 function is_active_for_site( $blog_id ) {
15118 if ( ! is_multisite() ) {
15119 // Not a multisite and this code is executed, means that the plugin is active.
15120 return true;
15121 }
15122
15123 if ( $this->is_theme() ) {
15124 // All themes are site level activated.
15125 return true;
15126 }
15127
15128 if ( $this->_is_network_active ) {
15129 // Plugin was network activated so it's active.
15130 return true;
15131 }
15132
15133 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15134 }
15135
15136 /**
15137 * @author Leo Fajardo (@leorw)
15138 * @since 2.0.0
15139 *
15140 * @return array Active & public sites collection.
15141 */
15142 static function get_sites() {
15143 if ( ! is_multisite() ) {
15144 return array();
15145 }
15146
15147 /**
15148 * For consistency with get_blog_list() which only return active public sites.
15149 *
15150 * @author Vova Feldman (@svovaf)
15151 */
15152 $args = array(
15153 /**
15154 * Commented out in order to handle the migration of site options whether the site is public or not.
15155 *
15156 * @author Leo Fajardo (@leorw)
15157 * @since 2.2.1
15158 */
15159 // 'public' => 1,
15160 'archived' => 0,
15161 'mature' => 0,
15162 'spam' => 0,
15163 'deleted' => 0,
15164 );
15165
15166 if ( function_exists( 'get_sites' ) ) {
15167 // For WP 4.6 and above.
15168 return get_sites( $args );
15169 } else if ( function_exists( 'wp_' . 'get_sites' ) ) {
15170 // For WP 3.7 to WP 4.5.
15171 /**
15172 * This is a hack suggested previously proposed by the TRT. Our SDK is compliant with older WP versions and we'd like to keep it that way.
15173 *
15174 * @todo Remove this hack once this false-positive error is removed from the Theme Sniffer.
15175 *
15176 * @since 2.3.3
15177 * @author Vova Feldman (@svovaf)
15178 */
15179 $fn = 'wp_' . 'get_sites';
15180 return $fn( $args );
15181 } else {
15182 // For WP 3.6 and below.
15183 return get_blog_list( 0, 'all' );
15184 }
15185 }
15186
15187 /**
15188 * Checks if a given blog is active.
15189 *
15190 * @author Vova Feldman (@svovaf)
15191 * @since 2.0.0
15192 *
15193 * @param $blog_id
15194 *
15195 * @return bool
15196 */
15197 private static function is_site_active( $blog_id ) {
15198 global $wpdb;
15199
15200 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15201
15202 if ( ! is_object( $blog_info ) ) {
15203 return false;
15204 }
15205
15206 return (
15207 true == $blog_info->public &&
15208 false == $blog_info->archived &&
15209 false == $blog_info->mature &&
15210 false == $blog_info->spam &&
15211 false == $blog_info->deleted
15212 );
15213 }
15214
15215 /**
15216 * Get a mapping between the site addresses to their blog IDs.
15217 *
15218 * @author Vova Feldman (@svovaf)
15219 * @since 2.0.0
15220 *
15221 * @return array {
15222 * @key string Site address without protocol with a trailing slash.
15223 * @value int Site's blog ID.
15224 * }
15225 */
15226 private function get_address_to_blog_map() {
15227 $sites = self::get_sites();
15228
15229 // Map site addresses to their blog IDs.
15230 $address_to_blog_map = array();
15231 foreach ( $sites as $site ) {
15232 $blog_id = self::get_site_blog_id( $site );
15233 $address = trailingslashit( fs_strip_url_protocol( get_site_url( $blog_id ) ) );
15234 $address_to_blog_map[ $address ] = $blog_id;
15235 }
15236
15237 return $address_to_blog_map;
15238 }
15239
15240 /**
15241 * Get a mapping between the site addresses to their blog IDs.
15242 *
15243 * @author Vova Feldman (@svovaf)
15244 * @since 2.0.0
15245 *
15246 * @return array {
15247 * @key int Site's blog ID.
15248 * @value FS_Site Associated install.
15249 * }
15250 */
15251 function get_blog_install_map() {
15252 $sites = self::get_sites();
15253
15254 // Map site blog ID to its install.
15255 $install_map = array();
15256
15257 foreach ( $sites as $site ) {
15258 $blog_id = self::get_site_blog_id( $site );
15259 $install = $this->get_install_by_blog_id( $blog_id );
15260
15261 if ( is_object( $install ) ) {
15262 $install_map[ $blog_id ] = $install;
15263 }
15264 }
15265
15266 return $install_map;
15267 }
15268
15269 /**
15270 * Gets a map of module IDs that the given user has opted-in to.
15271 *
15272 * @author Leo Fajardo (@leorw)
15273 * @since 2.1.0
15274 *
15275 * @param number $fs_user_id
15276 *
15277 * @return array {
15278 * @key number $plugin_id
15279 * @value bool Always true.
15280 * }
15281 */
15282 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15283 self::$_static_logger->entrance();
15284
15285 if ( ! is_multisite() ) {
15286 $installs = array_merge(
15287 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15288 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15289 );
15290 } else {
15291 $sites = self::get_sites();
15292
15293 $installs = array();
15294 foreach ( $sites as $site ) {
15295 $blog_id = self::get_site_blog_id( $site );
15296
15297 $installs = array_merge(
15298 $installs,
15299 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15300 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15301 );
15302 }
15303 }
15304
15305 $module_ids_map = array();
15306 foreach ( $installs as $install ) {
15307 if ( is_object( $install ) &&
15308 FS_Site::is_valid_id( $install->id ) &&
15309 FS_User::is_valid_id( $install->user_id ) &&
15310 ( $install->user_id == $fs_user_id )
15311 ) {
15312 $module_ids_map[ $install->plugin_id ] = true;
15313 }
15314 }
15315
15316 return $module_ids_map;
15317 }
15318
15319 /**
15320 * @author Leo Fajardo (@leorw)
15321 *
15322 * @return null|array {
15323 * 'install' => FS_Site Module's install,
15324 * 'blog_id' => string The associated blog ID.
15325 * }
15326 */
15327 function find_first_install() {
15328 $sites = self::get_sites();
15329
15330 foreach ( $sites as $site ) {
15331 $blog_id = self::get_site_blog_id( $site );
15332 $install = $this->get_install_by_blog_id( $blog_id );
15333
15334 if ( is_object( $install ) ) {
15335 return array(
15336 'install' => $install,
15337 'blog_id' => $blog_id
15338 );
15339 }
15340 }
15341
15342 return null;
15343 }
15344
15345 /**
15346 * Switches the Freemius site level context to a specified blog.
15347 *
15348 * @author Vova Feldman (@svovaf)
15349 * @since 2.0.0
15350 *
15351 * @param int $blog_id
15352 * @param FS_Site $install
15353 *
15354 * @return bool Since 2.3.1 returns if a switch was made.
15355 */
15356 function switch_to_blog( $blog_id, FS_Site $install = null ) {
15357 if ( ! is_numeric( $blog_id ) || $blog_id == $this->_context_is_network_or_blog_id ) {
15358 return false;
15359 }
15360
15361 switch_to_blog( $blog_id );
15362 $this->_context_is_network_or_blog_id = $blog_id;
15363
15364 self::$_accounts->set_site_blog_context( $blog_id );
15365 $this->_storage->set_site_blog_context( $blog_id );
15366 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15367
15368 $this->_site = is_object( $install ) ?
15369 $install :
15370 $this->get_install_by_blog_id( $blog_id );
15371
15372 $this->_user = false;
15373 $this->_licenses = false;
15374 $this->_license = null;
15375 $this->is_whitelabeled = null;
15376
15377 if ( is_object( $this->_site ) ) {
15378 // Try to fetch user from install.
15379 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15380
15381 if ( ! is_object( $this->_user ) &&
15382 FS_User::is_valid_id( $this->_storage->prev_user_id )
15383 ) {
15384 // Try to fetch previously saved user.
15385 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15386
15387 if ( ! is_object( $this->_user ) ) {
15388 // Fallback to network's user.
15389 $this->_user = $this->get_network_user();
15390 }
15391 }
15392
15393 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15394
15395 if ( ! empty( $all_plugin_licenses ) ) {
15396 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15397 $this->_license = null;
15398 } else {
15399 $license_found = false;
15400 foreach ( $all_plugin_licenses as $license ) {
15401 if ( $license->id == $this->_site->license_id ) {
15402 // License found.
15403 $this->_license = $license;
15404 $license_found = true;
15405 break;
15406 }
15407 }
15408
15409 if ( $license_found ) {
15410 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15411 }
15412 }
15413
15414 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15415 }
15416 }
15417
15418 unset( $this->_site_api );
15419 unset( $this->_user_api );
15420
15421 return false;
15422 }
15423
15424 /**
15425 * Restore the blog context to the blog that originally loaded the module.
15426 *
15427 * @author Vova Feldman (@svovaf)
15428 * @since 2.0.0
15429 */
15430 function restore_current_blog() {
15431 $this->switch_to_blog( $this->_blog_id );
15432 }
15433
15434 /**
15435 * @author Vova Feldman (@svovaf)
15436 * @since 2.0.0
15437 *
15438 * @param array|WP_Site $site
15439 *
15440 * @return int
15441 */
15442 static function get_site_blog_id( &$site ) {
15443 return ( $site instanceof WP_Site ) ?
15444 $site->blog_id :
15445 ( is_object( $site ) && isset( $site->userblog_id ) ?
15446 $site->userblog_id :
15447 $site['blog_id'] );
15448 }
15449
15450 /**
15451 * @author Leo Fajardo (@leorw)
15452 * @since 2.0.0
15453 *
15454 * @param array|WP_Site|null $site
15455 *
15456 * @return array
15457 */
15458 function get_site_info( $site = null ) {
15459 $this->_logger->entrance();
15460
15461 $switched = false;
15462
15463 if ( is_null( $site ) ) {
15464 $url = get_site_url();
15465 $name = get_bloginfo( 'name' );
15466 $blog_id = null;
15467 } else {
15468 $blog_id = self::get_site_blog_id( $site );
15469
15470 if ( get_current_blog_id() != $blog_id ) {
15471 switch_to_blog( $blog_id );
15472 $switched = true;
15473 }
15474
15475 if ( $site instanceof WP_Site ) {
15476 $url = $site->siteurl;
15477 $name = $site->blogname;
15478 } else {
15479 $url = get_site_url( $blog_id );
15480 $name = get_bloginfo( 'name' );
15481 }
15482 }
15483
15484 $info = array(
15485 'uid' => $this->get_anonymous_id( $blog_id ),
15486 'url' => $url,
15487 'title' => $name,
15488 'language' => get_bloginfo( 'language' ),
15489 'charset' => get_bloginfo( 'charset' ),
15490 );
15491
15492 if ( is_numeric( $blog_id ) ) {
15493 $info['blog_id'] = $blog_id;
15494 }
15495
15496 if ( $switched ) {
15497 restore_current_blog();
15498 }
15499
15500 return $info;
15501 }
15502
15503 /**
15504 * Load the module's install based on the blog ID.
15505 *
15506 * @author Vova Feldman (@svovaf)
15507 * @since 2.0.0
15508 *
15509 * @param int|null $blog_id
15510 *
15511 * @return FS_Site
15512 */
15513 function get_install_by_blog_id( $blog_id = null ) {
15514 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15515 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15516
15517 if ( is_object( $install ) &&
15518 is_numeric( $install->id ) &&
15519 is_numeric( $install->user_id ) &&
15520 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15521 ) {
15522 // Load site.
15523 $install = clone $install;
15524 }
15525
15526 return $install;
15527 }
15528
15529 /**
15530 * Check if module is installed on a specified site.
15531 *
15532 * @author Vova Feldman (@svovaf)
15533 * @since 2.0.0
15534 *
15535 * @param int|null $blog_id
15536 *
15537 * @return bool
15538 */
15539 function is_installed_on_site( $blog_id = null ) {
15540 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15541 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15542
15543 return (
15544 is_object( $install ) &&
15545 is_numeric( $install->id ) &&
15546 is_numeric( $install->user_id ) &&
15547 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15548 );
15549 }
15550
15551 /**
15552 * Check if super-admin connected at least one site via the network opt-in.
15553 *
15554 * @author Vova Feldman (@svovaf)
15555 * @since 2.0.0
15556 *
15557 * @return bool
15558 */
15559 function is_network_registered() {
15560 if ( ! $this->_is_network_active ) {
15561 return false;
15562 }
15563
15564 return FS_User::is_valid_id( $this->_storage->network_user_id );
15565 }
15566
15567 /**
15568 * Returns the main user associated with the network.
15569 *
15570 * @author Vova Feldman (@svovaf)
15571 * @since 2.0.0
15572 *
15573 * @return FS_User
15574 */
15575 function get_network_user() {
15576 if ( ! $this->_is_network_active ) {
15577 return null;
15578 }
15579
15580 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15581 self::_get_user_by_id( $this->_storage->network_user_id ) :
15582 null;
15583 }
15584
15585 /**
15586 * Returns the current context user or the network's main user.
15587 *
15588 * @author Vova Feldman (@svovaf)
15589 * @since 2.0.0
15590 *
15591 * @return FS_User
15592 */
15593 function get_current_or_network_user() {
15594 return ( $this->_user instanceof FS_User ) ?
15595 $this->_user :
15596 $this->get_network_user();
15597 }
15598
15599 /**
15600 * Returns the main install associated with the network.
15601 *
15602 * @author Vova Feldman (@svovaf)
15603 * @since 2.0.0
15604 *
15605 * @return FS_Site
15606 */
15607 function get_network_install() {
15608 if ( ! $this->_is_network_active ) {
15609 return null;
15610 }
15611
15612 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15613 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15614 null;
15615 }
15616
15617 /**
15618 * Returns the blog ID that is associated with the main install.
15619 *
15620 * @author Leo Fajardo (@leorw)
15621 * @since 2.0.0
15622 *
15623 * @return int|null
15624 */
15625 function get_network_install_blog_id() {
15626 if ( ! $this->_is_network_active ) {
15627 return null;
15628 }
15629
15630 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15631 $this->_storage->network_install_blog_id :
15632 null;
15633 }
15634
15635 /**
15636 * Returns the current context install or the network's main install.
15637 *
15638 * @author Vova Feldman (@svovaf)
15639 * @since 2.0.0
15640 *
15641 * @return FS_Site
15642 */
15643 function get_current_or_network_install() {
15644 return ( $this->_site instanceof FS_Site ) ?
15645 $this->_site :
15646 $this->get_network_install();
15647 }
15648
15649 /**
15650 * Check if executing a site level action from the network level admin.
15651 *
15652 * @author Vova Feldman (@svovaf)
15653 * @since 2.0.0
15654 *
15655 * @return false|int If yes, return the requested blog ID.
15656 */
15657 private function is_network_level_site_specific_action() {
15658 if ( ! $this->_is_network_active ) {
15659 return false;
15660 }
15661
15662 if ( ! fs_is_network_admin() ) {
15663 return false;
15664 }
15665
15666 $blog_id = fs_request_get( 'blog_id', '' );
15667
15668 return is_numeric( $blog_id ) ? $blog_id : false;
15669 }
15670
15671 /**
15672 * Check if executing an action from the network level admin.
15673 *
15674 * @author Vova Feldman (@svovaf)
15675 * @since 2.0.0
15676 *
15677 * @return bool
15678 */
15679 private function is_network_level_action() {
15680 return ( $this->_is_network_active && fs_is_network_admin() );
15681 }
15682
15683 /**
15684 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
15685 * 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.
15686 *
15687 * @author Vova Feldman (@svovaf)
15688 * @since 2.0.0
15689 *
15690 * @param int $context_blog_id
15691 */
15692 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
15693 $this->_logger->entrance();
15694
15695 if ( $this->_is_network_active ) {
15696 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
15697 $installs_map = $this->get_blog_install_map();
15698
15699 foreach ( $installs_map as $blog_id => $install ) {
15700 /**
15701 * @var FS_Site $install
15702 */
15703 if ( $context_blog_id == $blog_id ) {
15704 continue;
15705 }
15706
15707 if ( $install->user_id != $this->_storage->network_user_id ) {
15708 continue;
15709 }
15710
15711 // Switch reference to a blog that is opted-in and belong to the same super-admin.
15712 $this->_storage->network_install_blog_id = $blog_id;
15713 break;
15714 }
15715 }
15716 }
15717
15718 if ( $this->is_sync_cron_scheduled() &&
15719 $context_blog_id == $this->get_sync_cron_blog_id()
15720 ) {
15721 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
15722 }
15723
15724 if ( $this->is_install_sync_scheduled() &&
15725 $context_blog_id == $this->get_install_sync_cron_blog_id()
15726 ) {
15727 $this->schedule_install_sync( $context_blog_id );
15728 }
15729 }
15730
15731 /**
15732 * Executed after site deactivation, archive, or flag as spam.
15733 *
15734 * @author Vova Feldman (@svovaf)
15735 * @since 2.0.0
15736 *
15737 * @param int $context_blog_id
15738 */
15739 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
15740 $this->_logger->entrance();
15741
15742 $install = $this->get_install_by_blog_id( $context_blog_id );
15743
15744 if ( ! is_object( $install ) ) {
15745 // Site not connected.
15746 return;
15747 }
15748
15749 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15750
15751 $current_blog_id = get_current_blog_id();
15752
15753 $this->switch_to_blog( $context_blog_id );
15754
15755 // Send deactivation event.
15756 $this->sync_install( array(
15757 'is_active' => false,
15758 ) );
15759
15760 $this->switch_to_blog( $current_blog_id );
15761 }
15762
15763 /**
15764 * Executed after site deletion.
15765 *
15766 * @author Vova Feldman (@svovaf)
15767 * @since 2.0.0
15768 *
15769 * @param int $context_blog_id
15770 * @param bool $drop True if site's database tables should be dropped. Default is false.
15771 */
15772 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
15773 $this->_logger->entrance();
15774
15775 $install = $this->get_install_by_blog_id( $context_blog_id );
15776
15777 if ( ! is_object( $install ) ) {
15778 // Site not connected.
15779 return;
15780 }
15781
15782 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15783
15784 $current_blog_id = get_current_blog_id();
15785
15786 $this->switch_to_blog( $context_blog_id );
15787
15788 if ( $drop ) {
15789 // Delete install if dropping site DB.
15790 $this->delete_account_event();
15791 } else {
15792 // Send deactivation event.
15793 $this->sync_install( array(
15794 'is_active' => false,
15795 ) );
15796 }
15797
15798 $this->switch_to_blog( $current_blog_id );
15799 }
15800
15801 /**
15802 * Executed after site re-activation.
15803 *
15804 * @author Vova Feldman (@svovaf)
15805 * @since 2.0.0
15806 *
15807 * @param int $context_blog_id
15808 */
15809 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
15810 $this->_logger->entrance();
15811
15812 $install = $this->get_install_by_blog_id( $context_blog_id );
15813
15814 if ( ! is_object( $install ) ) {
15815 // Site not connected.
15816 return;
15817 }
15818
15819 if ( ! self::is_site_active( $context_blog_id ) ) {
15820 // Site not yet active (can be in spam mode, archived, deleted...).
15821 return;
15822 }
15823
15824 $current_blog_id = get_current_blog_id();
15825
15826 $this->switch_to_blog( $context_blog_id );
15827
15828 // Send re-activation event.
15829 $this->sync_install( array(
15830 'is_active' => true,
15831 ) );
15832
15833 $this->switch_to_blog( $current_blog_id );
15834 }
15835
15836 #endregion Multisite
15837
15838 /**
15839 * @author Leo Fajardo (@leorw)
15840 *
15841 * @param string $path
15842 * @param string $scheme
15843 * @param bool $network
15844 *
15845 * @return string
15846 */
15847 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
15848 return ( $this->_is_network_active && $network ) ?
15849 network_admin_url( $path, $scheme ) :
15850 admin_url( $path, $scheme );
15851 }
15852
15853 /**
15854 * Check if currently in a specified admin page.
15855 *
15856 * @author Vova Feldman (@svovaf)
15857 * @since 1.2.2.7
15858 *
15859 * @param string $page
15860 *
15861 * @return bool
15862 */
15863 function is_admin_page( $page ) {
15864 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
15865 }
15866
15867 /**
15868 * Check if currently in the product's main admin page.
15869 *
15870 * @author Vova Feldman (@svovaf)
15871 * @since 2.3.1
15872 *
15873 * @return bool
15874 */
15875 function is_main_admin_page() {
15876 return $this->is_admin_page( '' );
15877 }
15878
15879 /**
15880 * Get module's main admin setting page URL.
15881 *
15882 * @author Vova Feldman (@svovaf)
15883 * @since 1.2.2.7
15884 *
15885 * @return string
15886 */
15887 function main_menu_url() {
15888 return $this->_menu->main_menu_url();
15889 }
15890
15891 /**
15892 * Check if currently on the theme's setting page or
15893 * on any of the Freemius added pages (via tabs).
15894 *
15895 * @author Vova Feldman (@svovaf)
15896 * @since 1.2.2.7
15897 *
15898 * @return bool
15899 *
15900 * @deprecated Please use is_product_settings_page() instead;
15901 */
15902 function is_theme_settings_page() {
15903 return $this->is_product_settings_page();
15904 }
15905
15906 /**
15907 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
15908 *
15909 * @author Vova Feldman (@svovaf)
15910 * @since 1.2.2.7
15911 *
15912 * @return bool
15913 */
15914 function is_product_settings_page() {
15915 return fs_starts_with(
15916 fs_request_get( 'page', '', 'get' ),
15917 $this->_menu->get_slug()
15918 );
15919 }
15920
15921 /**
15922 * Plugin's account page + sync license URL.
15923 *
15924 * @author Vova Feldman (@svovaf)
15925 * @since 1.1.9.1
15926 *
15927 * @param bool|number $plugin_id
15928 * @param bool $add_action_nonce
15929 * @param array $params
15930 *
15931 * @return string
15932 */
15933 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
15934 if ( is_numeric( $plugin_id ) ) {
15935 $params['plugin_id'] = $plugin_id;
15936 }
15937
15938 return $this->get_account_url(
15939 $this->get_unique_affix() . '_sync_license',
15940 $params,
15941 $add_action_nonce
15942 );
15943 }
15944
15945 /**
15946 * Plugin's account URL.
15947 *
15948 * @author Vova Feldman (@svovaf)
15949 * @since 1.0.4
15950 *
15951 * @param bool|string $action
15952 * @param array $params
15953 *
15954 * @param bool $add_action_nonce
15955 *
15956 * @return string
15957 */
15958 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
15959 if ( is_string( $action ) ) {
15960 $params['fs_action'] = $action;
15961 }
15962
15963 self::require_pluggable_essentials();
15964
15965 return ( $add_action_nonce && is_string( $action ) ) ?
15966 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
15967 $this->_get_admin_page_url( 'account', $params );
15968 }
15969
15970 /**
15971 * @author Vova Feldman (@svovaf)
15972 * @since 1.2.0
15973 *
15974 * @param string $tab
15975 * @param bool $action
15976 * @param array $params
15977 * @param bool $add_action_nonce
15978 *
15979 * @return string
15980 *
15981 * @uses get_account_url()
15982 */
15983 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
15984 $params['tab'] = $tab;
15985
15986 return $this->get_account_url( $action, $params, $add_action_nonce );
15987 }
15988
15989 /**
15990 * Plugin's account URL.
15991 *
15992 * @author Vova Feldman (@svovaf)
15993 * @since 1.0.4
15994 *
15995 * @param bool|string $topic
15996 * @param bool|string $message
15997 *
15998 * @return string
15999 */
16000 function contact_url( $topic = false, $message = false ) {
16001 $params = array();
16002 if ( is_string( $topic ) ) {
16003 $params['topic'] = $topic;
16004 }
16005 if ( is_string( $message ) ) {
16006 $params['message'] = $message;
16007 }
16008
16009 if ( $this->is_addon() ) {
16010 $params['addon_id'] = $this->get_id();
16011
16012 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16013 } else {
16014 return $this->_get_admin_page_url( 'contact', $params );
16015 }
16016 }
16017
16018 /**
16019 * Add-on direct info URL.
16020 *
16021 * @author Vova Feldman (@svovaf)
16022 * @since 1.1.0
16023 *
16024 * @param string $slug
16025 *
16026 * @return string
16027 */
16028 function addon_url( $slug ) {
16029 return $this->_get_admin_page_url( 'addons', array(
16030 'slug' => $slug
16031 ) );
16032 }
16033
16034 /**
16035 * Add-ons URL.
16036 *
16037 * @author Vova Feldman (@svovaf)
16038 * @since 2.4.5
16039 *
16040 * @return string
16041 */
16042 function get_addons_url() {
16043 return $this->_get_admin_page_url( 'addons' );
16044 }
16045
16046 /* Logger
16047 ------------------------------------------------------------------------------------------------------------------*/
16048 /**
16049 * @param string $id
16050 * @param bool $prefix_slug
16051 *
16052 * @return FS_Logger
16053 */
16054 function get_logger( $id = '', $prefix_slug = true ) {
16055 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16056 }
16057
16058 /**
16059 * Note: This method is used externally so don't delete it.
16060 *
16061 * @param $id
16062 * @param bool $load_options
16063 * @param bool $prefix_slug
16064 *
16065 * @return FS_Option_Manager
16066 */
16067 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16068 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16069 }
16070
16071 /* Security
16072 ------------------------------------------------------------------------------------------------------------------*/
16073 private static function _encrypt( $str ) {
16074 if ( is_null( $str ) ) {
16075 return null;
16076 }
16077
16078 /**
16079 * The encrypt/decrypt functions are used to protect
16080 * the user from messing up with some of the sensitive
16081 * data stored for the module as a JSON in the database.
16082 *
16083 * I used the same suggested hack by the theme review team.
16084 * For more details, look at the function `Base64UrlDecode()`
16085 * in `./sdk/FreemiusBase.php`.
16086 *
16087 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16088 *
16089 * @author Vova Feldman (@svovaf)
16090 * @since 1.2.2
16091 */
16092 $fn = 'base64' . '_encode';
16093
16094 return $fn( $str );
16095 }
16096
16097 static function _decrypt( $str ) {
16098 if ( is_null( $str ) ) {
16099 return null;
16100 }
16101
16102 /**
16103 * The encrypt/decrypt functions are used to protect
16104 * the user from messing up with some of the sensitive
16105 * data stored for the module as a JSON in the database.
16106 *
16107 * I used the same suggested hack by the theme review team.
16108 * For more details, look at the function `Base64UrlDecode()`
16109 * in `./sdk/FreemiusBase.php`.
16110 *
16111 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16112 *
16113 * @author Vova Feldman (@svovaf)
16114 * @since 1.2.2
16115 */
16116 $fn = 'base64' . '_decode';
16117
16118 return $fn( $str );
16119 }
16120
16121 /**
16122 * @author Vova Feldman (@svovaf)
16123 * @since 1.0.5
16124 *
16125 * @param FS_Entity $entity
16126 *
16127 * @return FS_Entity Return an encrypted clone entity.
16128 */
16129 private static function _encrypt_entity( FS_Entity $entity ) {
16130 $clone = clone $entity;
16131 $props = get_object_vars( $entity );
16132
16133 foreach ( $props as $key => $val ) {
16134 $clone->{$key} = self::_encrypt( $val );
16135 }
16136
16137 return $clone;
16138 }
16139
16140 /**
16141 * @author Vova Feldman (@svovaf)
16142 * @since 1.0.5
16143 *
16144 * @param FS_Entity $entity
16145 *
16146 * @return FS_Entity Return an decrypted clone entity.
16147 */
16148 private static function decrypt_entity( FS_Entity $entity ) {
16149 $clone = clone $entity;
16150 $props = get_object_vars( $entity );
16151
16152 foreach ( $props as $key => $val ) {
16153 $clone->{$key} = self::_decrypt( $val );
16154 }
16155
16156 return $clone;
16157 }
16158
16159 /**
16160 * Tries to activate account based on POST params.
16161 *
16162 * @author Vova Feldman (@svovaf)
16163 * @since 1.0.2
16164 *
16165 * @deprecated Not in use, outdated.
16166 */
16167 function _activate_account() {
16168 if ( $this->is_registered() ) {
16169 // Already activated.
16170 return;
16171 }
16172
16173 self::_clean_admin_content_section();
16174
16175 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
16176 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
16177
16178 // Verify matching plugin details.
16179 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
16180 return;
16181 }
16182
16183 $user = new FS_User();
16184 $user->id = fs_request_get( 'user_id' );
16185 $user->public_key = fs_request_get( 'user_public_key' );
16186 $user->secret_key = fs_request_get( 'user_secret_key' );
16187 $user->email = fs_request_get( 'user_email' );
16188 $user->first = fs_request_get( 'user_first' );
16189 $user->last = fs_request_get( 'user_last' );
16190 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
16191
16192 $site = new FS_Site();
16193 $site->id = fs_request_get( 'install_id' );
16194 $site->public_key = fs_request_get( 'install_public_key' );
16195 $site->secret_key = fs_request_get( 'install_secret_key' );
16196 $site->plan_id = fs_request_get( 'plan_id' );
16197
16198 $plans = array();
16199 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
16200 foreach ( $plans_data as $p ) {
16201 $plan = new FS_Plugin_Plan( $p );
16202 if ( $site->plan_id == $plan->id ) {
16203 $plan->title = fs_request_get( 'plan_title' );
16204 $plan->name = fs_request_get( 'plan_name' );
16205 }
16206
16207 $plans[] = $plan;
16208 }
16209
16210 $this->_set_account( $user, $site, $plans );
16211
16212 // Reload the page with the keys.
16213 fs_redirect( $this->_get_admin_page_url() );
16214 }
16215 }
16216
16217 /**
16218 * @author Vova Feldman (@svovaf)
16219 * @since 1.0.7
16220 *
16221 * @param string $email
16222 *
16223 * @return FS_User|false
16224 */
16225 static function _get_user_by_email( $email ) {
16226 self::$_static_logger->entrance();
16227
16228 $email = trim( strtolower( $email ) );
16229
16230 $users = self::get_all_users();
16231
16232 if ( is_array( $users ) ) {
16233 foreach ( $users as $user ) {
16234 if ( $email === trim( strtolower( $user->email ) ) ) {
16235 return $user;
16236 }
16237 }
16238 }
16239
16240 return false;
16241 }
16242
16243 #----------------------------------------------------------------------------------
16244 #region Account (Loading, Updates & Activation)
16245 #----------------------------------------------------------------------------------
16246
16247 /***
16248 * Load account information (user + site).
16249 *
16250 * @author Vova Feldman (@svovaf)
16251 * @since 1.0.1
16252 */
16253 private function _load_account() {
16254 $this->_logger->entrance();
16255
16256 $this->do_action( 'before_account_load' );
16257
16258 $users = self::get_all_users();
16259 $plans = self::get_all_plans( $this->_module_type );
16260
16261 if ( $this->_logger->is_on() && is_admin() ) {
16262 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16263 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16264 }
16265
16266 $site = fs_is_network_admin() ?
16267 $this->get_network_install() :
16268 $this->get_install_by_blog_id();
16269
16270 if ( fs_is_network_admin() &&
16271 $this->is_network_active() &&
16272 ! is_object( $site ) &&
16273 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16274 ) {
16275 $first_install = $this->find_first_install();
16276
16277 if ( is_null( $first_install ) ) {
16278 unset( $this->_storage->network_install_blog_id );
16279 } else {
16280 $site = $first_install['install'];
16281 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16282 }
16283 }
16284
16285 if ( is_object( $site ) &&
16286 is_numeric( $site->id ) &&
16287 is_numeric( $site->user_id ) &&
16288 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16289 ) {
16290 // Load site.
16291 $this->_site = $site;
16292
16293 // Load plans.
16294 $this->_plans = $plans[ $this->_slug ];
16295 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16296 $this->_sync_plans();
16297 } else {
16298 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16299 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16300 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16301 } else {
16302 unset( $this->_plans[ $i ] );
16303 }
16304 }
16305 }
16306 }
16307
16308 $user = null;
16309 if ( fs_is_network_admin() && $this->_is_network_active ) {
16310 $user = $this->get_network_user();
16311 }
16312
16313 if ( is_object( $user ) ) {
16314 $this->_user = clone $user;
16315 } else if ( $this->_site ) {
16316 $user = self::_get_user_by_id( $this->_site->user_id );
16317
16318 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16319 /**
16320 * Try to load the previous owner. This recovery is used for the following use-case:
16321 * 1. Opt-in
16322 * 2. Cloning site1 to site2
16323 * 3. Ownership switch in site1 (same applies for site2)
16324 * 4. Install data sync on site2
16325 * 5. Now site2's install is associated with the new owner which does not exists locally.
16326 */
16327 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16328 }
16329
16330 if ( ! is_object( $user ) ) {
16331 /**
16332 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16333 */
16334 $user = $this->sync_user_by_current_install();
16335 }
16336
16337 $this->_user = ( $user instanceof FS_User ) ?
16338 clone $user :
16339 null;
16340 }
16341
16342 if ( is_object( $this->_user ) ) {
16343 // Load licenses.
16344 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16345 }
16346
16347 if ( is_object( $this->_site ) ) {
16348 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16349
16350 if ( $this->_site->version != $this->get_plugin_version() ) {
16351 // If stored install version is different than current installed plugin version,
16352 // then update plugin version event.
16353 $this->update_plugin_version_event();
16354 }
16355 }
16356
16357 if ( true === $this->_storage->require_license_activation &&
16358 ! fs_request_get_bool( 'require_license', true )
16359 ) {
16360 $this->_storage->require_license_activation = false;
16361 }
16362
16363 if ( $this->is_theme() ) {
16364 $this->_register_account_hooks();
16365 }
16366 }
16367
16368 /**
16369 * Special user recovery mechanism.
16370 *
16371 * @author Vova Feldman (@svovaf)
16372 * @since 2.0.0
16373 *
16374 * @param number|null $site_user_id
16375 *
16376 * @return \FS_User|mixed
16377 */
16378 private function sync_user_by_current_install( $site_user_id = null ) {
16379 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16380 $site_user_id :
16381 $this->_site->user_id;
16382
16383 $api = $this->get_api_site_scope();
16384
16385 $uid = $this->get_anonymous_id();
16386 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16387
16388 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16389
16390 if ( $this->is_api_result_entity( $result ) ) {
16391 $user = new FS_User( $result );
16392 $this->_user = $user;
16393 $this->_store_user();
16394
16395 return $user;
16396 }
16397
16398 $error_code = FS_Api::get_error_code( $result );
16399
16400 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16401 /**
16402 * Those API errors will continue coming and are not recoverable with the
16403 * current site's data. Therefore, extend the API call's cached result to 7 days.
16404 */
16405 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16406 }
16407
16408 return $result;
16409 }
16410
16411 /**
16412 * @author Vova Feldman (@svovaf)
16413 * @since 1.0.1
16414 *
16415 * @param FS_User $user
16416 * @param FS_Site $site
16417 * @param bool|array $plans
16418 */
16419 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16420 $site->user_id = $user->id;
16421
16422 $this->_site = $site;
16423 $this->_user = $user;
16424 if ( false !== $plans ) {
16425 $this->_plans = $plans;
16426 }
16427
16428 $this->send_install_update();
16429
16430 $this->_store_account();
16431
16432 }
16433
16434 /**
16435 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16436 * Each version is trimmed after the 16th char.
16437 *
16438 * @author Vova Feldman (@svovaf)
16439 * @since 2.2.1
16440 *
16441 * @return array
16442 */
16443 private function get_versions() {
16444 $versions = array();
16445 $versions['platform_version'] = get_bloginfo( 'version' );
16446 $versions['sdk_version'] = $this->version;
16447 $versions['programming_language_version'] = phpversion();
16448
16449 foreach ( $versions as $k => $version ) {
16450 if ( is_string( $versions[ $k ] ) && ! empty( $versions[ $k ] ) ) {
16451 $versions[ $k ] = substr( $versions[ $k ], 0, 16 );
16452 }
16453 }
16454
16455 return $versions;
16456 }
16457
16458 /**
16459 * @author Leo Fajardo (@leorw)
16460 * @since 2.3.0
16461 *
16462 * @return bool
16463 */
16464 function has_beta_update() {
16465 return (
16466 ! empty( $this->_storage->beta_data ) &&
16467 ( true === $this->_storage->beta_data['is_beta'] ) &&
16468 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16469 );
16470 }
16471
16472 /**
16473 * @author Leo Fajardo (@leorw)
16474 * @since 2.3.0
16475 *
16476 * @return bool
16477 */
16478 function is_beta() {
16479 return (
16480 ! empty( $this->_storage->beta_data ) &&
16481 ( true === $this->_storage->beta_data['is_beta'] ) &&
16482 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16483 );
16484 }
16485
16486 /**
16487 * @author Vova Feldman (@svovaf)
16488 * @since 1.1.7.4
16489 *
16490 * @param array $override_with
16491 * @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.
16492 *
16493 * @return array
16494 */
16495 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
16496 $this->_logger->entrance();
16497
16498 $current_user = self::_get_current_wp_user();
16499
16500 $activation_action = $this->get_unique_affix() . '_activate_new';
16501 $return_url = $this->is_anonymous() ?
16502 // If skipped already, then return to the account page.
16503 $this->get_account_url( $activation_action, array(), false ) :
16504 // Return to the module's main page.
16505 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16506
16507 $versions = $this->get_versions();
16508
16509 $params = array_merge( $versions, array(
16510 'user_firstname' => $current_user->user_firstname,
16511 'user_lastname' => $current_user->user_lastname,
16512 'user_nickname' => $current_user->user_nicename,
16513 'user_email' => $current_user->user_email,
16514 'user_ip' => WP_FS__REMOTE_ADDR,
16515 'plugin_slug' => $this->_slug,
16516 'plugin_id' => $this->get_id(),
16517 'plugin_public_key' => $this->get_public_key(),
16518 'plugin_version' => $this->get_plugin_version(),
16519 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16520 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16521 'account',
16522 array( 'fs_action' => 'sync_user' )
16523 ), 'sync_user' ),
16524 'is_premium' => $this->is_premium(),
16525 'is_active' => true,
16526 'is_uninstalled' => false,
16527 ) );
16528
16529 if ( $this->is_addon() ) {
16530 $parent_fs = $this->get_parent_instance();
16531
16532 $params['parent_plugin_slug'] = $parent_fs->_slug;
16533 $params['parent_plugin_id'] = $parent_fs->get_id();
16534 }
16535
16536 if ( true === $network_level_or_blog_id ) {
16537 if ( ! isset( $override_with['sites'] ) ) {
16538 $params['sites'] = $this->get_sites_for_network_level_optin();
16539 }
16540 } else {
16541 $site = is_numeric( $network_level_or_blog_id ) ?
16542 array( 'blog_id' => $network_level_or_blog_id ) :
16543 null;
16544
16545 $site = $this->get_site_info( $site );
16546
16547 $params = array_merge( $params, array(
16548 'site_uid' => $site['uid'],
16549 'site_url' => $site['url'],
16550 'site_name' => $site['title'],
16551 'language' => $site['language'],
16552 'charset' => $site['charset'],
16553 ) );
16554 }
16555
16556 if ( $this->is_pending_activation() &&
16557 ! empty( $this->_storage->pending_license_key )
16558 ) {
16559 $params['license_key'] = $this->_storage->pending_license_key;
16560 }
16561
16562 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
16563 // Even though rand() is known for its security issues,
16564 // the timestamp adds another layer of protection.
16565 // It would be very hard for an attacker to get the secret key form here.
16566 // Plus, this should never run in production since the secret should never
16567 // be included in the production version.
16568 $params['ts'] = WP_FS__SCRIPT_START_TIME;
16569 $params['salt'] = md5( uniqid( rand() ) );
16570 $params['secure'] = md5(
16571 $params['ts'] .
16572 $params['salt'] .
16573 $this->get_secret_key()
16574 );
16575 }
16576
16577 return array_merge( $params, $override_with );
16578 }
16579
16580 /**
16581 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
16582 * 2. If there was an API error, return the API result.
16583 *
16584 * @author Vova Feldman (@svovaf)
16585 * @since 1.1.7.4
16586 *
16587 * @param string|bool $email
16588 * @param string|bool $first
16589 * @param string|bool $last
16590 * @param string|bool $license_key
16591 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
16592 * In this case, the user and site info will be sent to the server but no
16593 * data will be saved to the WP installation's database.
16594 * @param number|bool $trial_plan_id
16595 * @param bool $is_disconnected Whether or not to opt in without tracking.
16596 * @param null|bool $is_marketing_allowed
16597 * @param array $sites If network-level opt-in, an array of containing details of sites.
16598 *
16599 * @return string|object
16600 * @use WP_Error
16601 */
16602 function opt_in(
16603 $email = false,
16604 $first = false,
16605 $last = false,
16606 $license_key = false,
16607 $is_uninstall = false,
16608 $trial_plan_id = false,
16609 $is_disconnected = false,
16610 $is_marketing_allowed = null,
16611 $sites = array()
16612 ) {
16613 $this->_logger->entrance();
16614
16615 if ( false === $email ) {
16616 $current_user = self::_get_current_wp_user();
16617 $email = $current_user->user_email;
16618 }
16619
16620 /**
16621 * @since 1.2.1 If activating with license key, ignore the context-user
16622 * since the user will be automatically loaded from the license.
16623 */
16624 if ( empty( $license_key ) ) {
16625 // Clean up pending license if opt-ing in again.
16626 $this->_storage->remove( 'pending_license_key' );
16627
16628 if ( ! $is_uninstall ) {
16629 $fs_user = Freemius::_get_user_by_email( $email );
16630 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
16631 return $this->install_with_user(
16632 $fs_user,
16633 false,
16634 $trial_plan_id,
16635 true,
16636 true,
16637 $sites
16638 );
16639 }
16640 }
16641 }
16642
16643 $user_info = array();
16644 if ( ! empty( $email ) ) {
16645 $user_info['user_email'] = $email;
16646 }
16647 if ( ! empty( $first ) ) {
16648 $user_info['user_firstname'] = $first;
16649 }
16650 if ( ! empty( $last ) ) {
16651 $user_info['user_lastname'] = $last;
16652 }
16653
16654 if ( ! empty( $sites ) ) {
16655 $is_network = true;
16656
16657 $user_info['sites'] = $sites;
16658 } else {
16659 $is_network = false;
16660 }
16661
16662 $params = $this->get_opt_in_params( $user_info, $is_network );
16663
16664 $filtered_license_key = false;
16665 if ( is_string( $license_key ) ) {
16666 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
16667 $params['license_key'] = $filtered_license_key;
16668 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
16669 $params['trial_plan_id'] = $trial_plan_id;
16670 }
16671
16672 if ( $is_uninstall ) {
16673 $params['uninstall_params'] = array(
16674 'reason_id' => $this->_storage->uninstall_reason->id,
16675 'reason_info' => $this->_storage->uninstall_reason->info
16676 );
16677 }
16678
16679 if ( isset( $params['license_key'] ) ) {
16680 $fs_user = Freemius::_get_user_by_email( $email );
16681
16682 if ( is_object( $fs_user ) ) {
16683 /**
16684 * If opting in with a context license and the context WP Admin user already opted in
16685 * before from the current site, add the user context security params to avoid the
16686 * unnecessary email activation when the context license is owned by the same context user.
16687 *
16688 * @author Leo Fajardo (@leorw)
16689 * @since 1.2.3
16690 */
16691 $params = array_merge( $params, FS_Security::instance()->get_context_params(
16692 $fs_user,
16693 false,
16694 'install_with_existing_user'
16695 ) );
16696 }
16697 }
16698
16699 if ( is_bool( $is_marketing_allowed ) ) {
16700 $params['is_marketing_allowed'] = $is_marketing_allowed;
16701 }
16702
16703 $params['is_disconnected'] = $is_disconnected;
16704 $params['format'] = 'json';
16705
16706 $request = array(
16707 'method' => 'POST',
16708 'body' => $params,
16709 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
16710 );
16711
16712 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
16713 $response = self::safe_remote_post( $url, $request );
16714
16715 if ( is_wp_error( $response ) ) {
16716 /**
16717 * @var WP_Error $response
16718 */
16719 $result = new stdClass();
16720
16721 $error_code = $response->get_error_code();
16722 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
16723
16724 $result->error = (object) array(
16725 'type' => $error_type,
16726 'message' => $response->get_error_message(),
16727 'code' => $error_code,
16728 'http' => 402
16729 );
16730
16731 $this->maybe_modify_api_curl_error_message( $result );
16732
16733 return $result;
16734 }
16735
16736 // Module is being uninstalled, don't handle the returned data.
16737 if ( $is_uninstall ) {
16738 return true;
16739 }
16740
16741 /**
16742 * 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.
16743 *
16744 * @author Vova Feldman (@svovaf)
16745 * @since 1.2.3
16746 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
16747 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
16748 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
16749 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
16750 */
16751 $decoded = is_string( $response['body'] ) ?
16752 json_decode( $response['body'] ) :
16753 null;
16754
16755 if ( empty( $decoded ) ) {
16756 return false;
16757 }
16758
16759 if ( ! $this->is_api_result_object( $decoded ) ) {
16760 if ( ! empty( $params['license_key'] ) ) {
16761 // Pass the fully entered license key to the failure handler.
16762 $params['license_key'] = $license_key;
16763 }
16764
16765 return $is_uninstall ?
16766 $decoded :
16767 $this->apply_filters( 'after_install_failure', $decoded, $params );
16768 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
16769 if ( $is_network ) {
16770 $site_ids = array();
16771 foreach ( $sites as $site ) {
16772 $site_ids[] = $site['blog_id'];
16773 }
16774
16775 /**
16776 * Store the sites so that they can be installed once the user has clicked on the activation link
16777 * in the email.
16778 *
16779 * @author Leo Fajardo (@leorw)
16780 */
16781 $this->_storage->pending_sites_info = array(
16782 'blog_ids' => $site_ids,
16783 'license_key' => $license_key,
16784 'trial_plan_id' => $trial_plan_id
16785 );
16786 }
16787
16788 // Pending activation, add message.
16789 return $this->set_pending_confirmation(
16790 ( isset( $decoded->email ) ?
16791 $decoded->email :
16792 true ),
16793 false,
16794 $filtered_license_key,
16795 ! empty( $params['trial_plan_id'] )
16796 );
16797 } else if ( isset( $decoded->install_secret_key ) ) {
16798 return $this->install_with_new_user(
16799 $decoded->user_id,
16800 $decoded->user_public_key,
16801 $decoded->user_secret_key,
16802 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
16803 $decoded->is_marketing_allowed :
16804 null ),
16805 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
16806 $decoded->is_extensions_tracking_allowed :
16807 null ),
16808 $decoded->install_id,
16809 $decoded->install_public_key,
16810 $decoded->install_secret_key,
16811 false
16812 );
16813 } else if ( is_array( $decoded->installs ) ) {
16814 return $this->install_many_with_new_user(
16815 $decoded->user_id,
16816 $decoded->user_public_key,
16817 $decoded->user_secret_key,
16818 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
16819 $decoded->is_marketing_allowed :
16820 null ),
16821 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
16822 $decoded->is_extensions_tracking_allowed :
16823 null ),
16824 $decoded->installs,
16825 false
16826 );
16827 }
16828
16829 return $decoded;
16830 }
16831
16832 /**
16833 * Set user and site identities.
16834 *
16835 * @author Vova Feldman (@svovaf)
16836 * @since 1.0.9
16837 *
16838 * @param FS_User $user
16839 * @param FS_Site $site
16840 * @param bool $redirect
16841 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
16842 * redirect (or return a URL) to the account page with a special parameter to
16843 * trigger the auto installation processes.
16844 *
16845 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16846 */
16847 function setup_account(
16848 FS_User $user,
16849 FS_Site $site,
16850 $redirect = true,
16851 $auto_install = false
16852 ) {
16853 return $this->setup_network_account(
16854 $user,
16855 array( $site ),
16856 $redirect,
16857 $auto_install,
16858 false
16859 );
16860 }
16861
16862 /**
16863 * Set user and site identities.
16864 *
16865 * @author Vova Feldman (@svovaf)
16866 * @since 2.0.0
16867 *
16868 * @param FS_User $user
16869 * @param FS_Site[] $installs
16870 * @param bool $redirect
16871 * @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.
16872 * @param bool $is_network_level_opt_in
16873 *
16874 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16875 */
16876 function setup_network_account(
16877 FS_User $user,
16878 array $installs,
16879 $redirect = true,
16880 $auto_install = false,
16881 $is_network_level_opt_in = true
16882 ) {
16883 $first_install = $installs[0];
16884
16885 $this->_user = $user;
16886 $this->_site = $first_install;
16887
16888 $this->_sync_plans();
16889
16890 if ( $this->_storage->handle_gdpr_admin_notice &&
16891 $this->should_handle_gdpr_admin_notice() &&
16892 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
16893 ) {
16894 /**
16895 * Clear user lock after an opt-in.
16896 */
16897 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
16898 FS_User_Lock::instance()->unlock();
16899 }
16900
16901 if ( 1 < count( $installs ) ) {
16902 // Only network level opt-in can have more than one install.
16903 $is_network_level_opt_in = true;
16904 }
16905 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
16906 // If Freemius was OFF before, turn it on.
16907 $this->turn_on();
16908
16909 $this->handle_account_connection(
16910 $installs,
16911 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
16912 );
16913
16914 if ( is_numeric( $first_install->license_id ) ) {
16915 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
16916 }
16917
16918 $this->_admin_notices->remove_sticky( 'connect_account' );
16919
16920 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
16921 // Remove pending activation sticky notice (if still exist).
16922 $this->_admin_notices->remove_sticky( 'activation_pending' );
16923
16924 // Remove plugin from pending activation mode.
16925 unset( $this->_storage->is_pending_activation );
16926
16927 if ( ! $this->is_paying_or_trial() ) {
16928 $this->_admin_notices->add_sticky(
16929 sprintf( $this->get_text_inline( '%s activation was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
16930 'activation_complete'
16931 );
16932 }
16933 }
16934
16935 if ( $this->is_paying_or_trial() ) {
16936 if ( ! $this->is_premium() ||
16937 ! $this->has_premium_version() ||
16938 ! $this->has_settings_menu()
16939 ) {
16940 if ( $this->is_paying() ) {
16941 $this->_admin_notices->add_sticky(
16942 sprintf(
16943 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
16944 $this->get_plan_title()
16945 ) . $this->get_complete_upgrade_instructions(),
16946 'plan_upgraded',
16947 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16948 );
16949 } else {
16950 $trial_plan = $this->get_trial_plan();
16951
16952 $this->_admin_notices->add_sticky(
16953 sprintf(
16954 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
16955 '<i>' . $this->get_plugin_name() . '</i>'
16956 ) . $this->get_complete_upgrade_instructions( $trial_plan->title ),
16957 'trial_started',
16958 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16959 );
16960 }
16961 }
16962
16963 $this->_admin_notices->remove_sticky( array(
16964 'trial_promotion',
16965 ) );
16966 }
16967
16968 $plugin_id = fs_request_get( 'plugin_id', false );
16969
16970 // Store activation time ONLY for plugins & themes (not add-ons).
16971 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
16972 if ( empty( $this->_storage->activation_timestamp ) ) {
16973 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
16974 }
16975 }
16976
16977 $next_page = '';
16978
16979 $extra = array();
16980 if ( $auto_install ) {
16981 $extra['auto_install'] = 'true';
16982 }
16983
16984 if ( is_numeric( $plugin_id ) ) {
16985 /**
16986 * @author Leo Fajardo (@leorw)
16987 * @since 1.2.1.6
16988 *
16989 * Also sync the license after an anonymous user subscribes.
16990 */
16991 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
16992 // Add-on was installed - sync license right after install.
16993 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
16994 }
16995 } else {
16996 /**
16997 * @author Vova Feldman (@svovaf)
16998 * @since 1.1.9 If site installed with a valid license, sync license.
16999 */
17000 if ( $this->is_paying() ) {
17001 $this->_sync_plugin_license(
17002 true,
17003 // Installs data is already synced in the beginning of this method directly or via _set_account().
17004 false
17005 );
17006 }
17007
17008 // Reload the page with the keys.
17009 $next_page = $this->is_anonymous() ?
17010 // If user previously skipped, redirect to account page.
17011 $this->get_account_url( false, $extra ) :
17012 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17013 }
17014
17015 if ( ! empty( $next_page ) && $redirect ) {
17016 fs_redirect( $next_page );
17017 }
17018
17019 return $next_page;
17020 }
17021
17022 /**
17023 * Install plugin with new user information after approval.
17024 *
17025 * @author Vova Feldman (@svovaf)
17026 * @since 1.0.7
17027 */
17028 function _install_with_new_user() {
17029 $this->_logger->entrance();
17030
17031 if ( $this->is_registered() ) {
17032 return;
17033 }
17034
17035 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17036 // @todo This logic should be improved because it's executed on every load of a theme.
17037 $this->is_theme()
17038 ) {
17039 // check_admin_referer( $this->_slug . '_activate_new' );
17040
17041 if ( fs_request_has( 'user_secret_key' ) ) {
17042 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17043 $pending_sites_info = $this->_storage->pending_sites_info;
17044
17045 $this->install_many_pending_with_user(
17046 fs_request_get( 'user_id' ),
17047 fs_request_get( 'user_public_key' ),
17048 fs_request_get( 'user_secret_key' ),
17049 fs_request_get_bool( 'is_marketing_allowed', null ),
17050 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17051 $pending_sites_info['blog_ids'],
17052 $pending_sites_info['license_key'],
17053 $pending_sites_info['trial_plan_id']
17054 );
17055 } else {
17056 $this->install_with_new_user(
17057 fs_request_get( 'user_id' ),
17058 fs_request_get( 'user_public_key' ),
17059 fs_request_get( 'user_secret_key' ),
17060 fs_request_get_bool( 'is_marketing_allowed', null ),
17061 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17062 fs_request_get( 'install_id' ),
17063 fs_request_get( 'install_public_key' ),
17064 fs_request_get( 'install_secret_key' ),
17065 true,
17066 fs_request_get_bool( 'auto_install' )
17067 );
17068 }
17069 } else if ( fs_request_has( 'pending_activation' ) ) {
17070 $this->set_pending_confirmation( fs_request_get( 'user_email' ), true );
17071 }
17072 }
17073 }
17074
17075 /**
17076 * @author Vova Feldman (@svovaf)
17077 * @since 2.0.0
17078 *
17079 * @param number $id
17080 * @param string $public_key
17081 * @param string $secret_key
17082 *
17083 * @return \FS_User
17084 */
17085 private function setup_user( $id, $public_key, $secret_key ) {
17086 $user = self::_get_user_by_id( $id );
17087
17088 if ( is_object( $user ) ) {
17089 $this->_user = $user;
17090 } else {
17091 $user = new FS_User();
17092 $user->id = $id;
17093 $user->public_key = $public_key;
17094 $user->secret_key = $secret_key;
17095
17096 $this->_user = $user;
17097 $user_result = $this->get_api_user_scope()->get();
17098 $user = new FS_User( $user_result );
17099
17100 $this->_user = $user;
17101 $this->_store_user();
17102 }
17103
17104 return $user;
17105 }
17106
17107 /**
17108 * Install plugin with new user.
17109 *
17110 * @author Vova Feldman (@svovaf)
17111 * @since 1.1.7.4
17112 *
17113 * @param number $user_id
17114 * @param string $user_public_key
17115 * @param string $user_secret_key
17116 * @param bool|null $is_marketing_allowed
17117 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17118 * @param number $install_id
17119 * @param string $install_public_key
17120 * @param string $install_secret_key
17121 * @param bool $redirect
17122 * @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.
17123 *
17124 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17125 */
17126 private function install_with_new_user(
17127 $user_id,
17128 $user_public_key,
17129 $user_secret_key,
17130 $is_marketing_allowed,
17131 $is_extensions_tracking_allowed,
17132 $install_id,
17133 $install_public_key,
17134 $install_secret_key,
17135 $redirect = true,
17136 $auto_install = false
17137 ) {
17138 /**
17139 * This method is also executed after opting in with a license key since the
17140 * license can be potentially associated with a different owner.
17141 *
17142 * @since 2.0.0
17143 */
17144 $user = self::_get_user_by_id( $user_id );
17145
17146 if ( ! is_object( $user ) ) {
17147 $user = new FS_User();
17148 $user->id = $user_id;
17149 $user->public_key = $user_public_key;
17150 $user->secret_key = $user_secret_key;
17151
17152 $this->_user = $user;
17153 $user_result = $this->get_api_user_scope()->get();
17154 $user = new FS_User( $user_result );
17155 }
17156
17157 $this->_user = $user;
17158
17159 $site = new FS_Site();
17160 $site->id = $install_id;
17161 $site->public_key = $install_public_key;
17162 $site->secret_key = $install_secret_key;
17163
17164 $this->_site = $site;
17165 $site_result = $this->get_api_site_scope()->get();
17166 $site = new FS_Site( $site_result );
17167 $this->_site = $site;
17168
17169 if ( ! is_null( $is_marketing_allowed ) ) {
17170 $this->disable_opt_in_notice_and_lock_user();
17171 }
17172
17173 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17174
17175 return $this->setup_account(
17176 $this->_user,
17177 $this->_site,
17178 $redirect,
17179 $auto_install
17180 );
17181 }
17182
17183 /**
17184 * Install plugin with user.
17185 *
17186 * @author Leo Fajardo (@leorw)
17187 * @since 2.0.0
17188 *
17189 * @param number $user_id
17190 * @param string $user_public_key
17191 * @param string $user_secret_key
17192 * @param bool|null $is_marketing_allowed
17193 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17194 * @param array $site_ids
17195 * @param bool $license_key
17196 * @param bool $trial_plan_id
17197 * @param bool $redirect
17198 *
17199 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17200 */
17201 private function install_many_pending_with_user(
17202 $user_id,
17203 $user_public_key,
17204 $user_secret_key,
17205 $is_marketing_allowed,
17206 $is_extensions_tracking_allowed,
17207 $site_ids,
17208 $license_key = false,
17209 $trial_plan_id = false,
17210 $redirect = true
17211 ) {
17212 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17213
17214 if ( ! is_null( $is_marketing_allowed ) ) {
17215 $this->disable_opt_in_notice_and_lock_user();
17216 }
17217
17218 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17219
17220 $sites = array();
17221 foreach ( $site_ids as $site_id ) {
17222 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17223 }
17224
17225 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17226 }
17227
17228 /**
17229 * Multi-site install with a new user.
17230 *
17231 * @author Vova Feldman (@svovaf)
17232 * @since 2.0.0
17233 *
17234 * @param number $user_id
17235 * @param string $user_public_key
17236 * @param string $user_secret_key
17237 * @param bool|null $is_marketing_allowed
17238 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17239 * @param object[] $installs
17240 * @param bool $redirect
17241 * @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.
17242 *
17243 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17244 */
17245 private function install_many_with_new_user(
17246 $user_id,
17247 $user_public_key,
17248 $user_secret_key,
17249 $is_marketing_allowed,
17250 $is_extensions_tracking_allowed,
17251 array $installs,
17252 $redirect = true,
17253 $auto_install = false
17254 ) {
17255 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17256
17257 if ( ! is_null( $is_marketing_allowed ) ) {
17258 $this->disable_opt_in_notice_and_lock_user();
17259 }
17260
17261 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17262
17263 $install_ids = array();
17264
17265 foreach ( $installs as $install ) {
17266 $install_ids[] = $install->id;
17267 }
17268
17269 $left = count( $install_ids );
17270 $offset = 0;
17271
17272 $installs = array();
17273 while ( $left > 0 ) {
17274 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, 25 ) ) );
17275
17276 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17277 // @todo Handle API error.
17278 }
17279
17280 $installs = array_merge( $installs, $result->installs );
17281
17282 $left -= 25;
17283 }
17284
17285 foreach ( $installs as &$install ) {
17286 $install = new FS_Site( $install );
17287 }
17288
17289 return $this->setup_network_account(
17290 $this->_user,
17291 $installs,
17292 $redirect,
17293 $auto_install
17294 );
17295 }
17296
17297 /**
17298 * @author Vova Feldman (@svovaf)
17299 * @since 1.1.7.4
17300 *
17301 * @param string|bool $email
17302 * @param bool $redirect
17303 * @param string|bool $license_key Since 1.2.1.5
17304 * @param bool $is_pending_trial Since 1.2.1.5
17305 *
17306 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17307 */
17308 private function set_pending_confirmation(
17309 $email = false,
17310 $redirect = true,
17311 $license_key = false,
17312 $is_pending_trial = false
17313 ) {
17314 if ( $this->_ignore_pending_mode ) {
17315 /**
17316 * If explicitly asked to ignore pending mode, set to anonymous mode
17317 * if require confirmation before finalizing the opt-in.
17318 *
17319 * @author Vova Feldman
17320 * @since 1.2.1.6
17321 */
17322 $this->skip_connection( null, fs_is_network_admin() );
17323 } else {
17324 // Install must be activated via email since
17325 // user with the same email already exist.
17326 $this->_storage->is_pending_activation = true;
17327 $this->_add_pending_activation_notice( $email, $is_pending_trial );
17328 }
17329
17330 if ( ! empty( $license_key ) ) {
17331 $this->_storage->pending_license_key = $license_key;
17332 }
17333
17334 // Remove the opt-in sticky notice.
17335 $this->_admin_notices->remove_sticky( array(
17336 'connect_account',
17337 'trial_promotion',
17338 ) );
17339
17340 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17341
17342 // Reload the page with with pending activation message.
17343 if ( $redirect ) {
17344 fs_redirect( $next_page );
17345 }
17346
17347 return $next_page;
17348 }
17349
17350 /**
17351 * Install plugin with current logged WP user info.
17352 *
17353 * @author Vova Feldman (@svovaf)
17354 * @since 1.0.7
17355 */
17356 function _install_with_current_user() {
17357 $this->_logger->entrance();
17358
17359 if ( $this->is_registered() ) {
17360 return;
17361 }
17362
17363 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17364 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
17365
17366 /**
17367 * @author Vova Feldman (@svovaf)
17368 * @since 1.1.9 Add license key if given.
17369 */
17370 $license_key = fs_request_get( 'license_secret_key' );
17371
17372 $this->update_extensions_tracking_flag( fs_request_get_bool( 'is_extensions_tracking_allowed', null ) );
17373
17374 $this->install_with_current_user( $license_key );
17375 }
17376 }
17377
17378
17379 /**
17380 * @author Vova Feldman (@svovaf)
17381 * @since 1.1.7.4
17382 *
17383 * @param string|bool $license_key
17384 * @param number|bool $trial_plan_id
17385 * @param array $sites Since 2.0.0
17386 * @param bool $redirect
17387 *
17388 * @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.
17389 */
17390 private function install_with_current_user(
17391 $license_key = false,
17392 $trial_plan_id = false,
17393 $sites = array(),
17394 $redirect = true
17395 ) {
17396 // Get current logged WP user.
17397 $current_user = self::_get_current_wp_user();
17398
17399 // Find the relevant FS user by the email.
17400 $user = self::_get_user_by_email( $current_user->user_email );
17401
17402 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17403 }
17404
17405 /**
17406 * @author Vova Feldman (@svovaf)
17407 * @since 2.0.0
17408 *
17409 * @param \FS_User $user
17410 * @param string|bool $license_key
17411 * @param number|bool $trial_plan_id
17412 * @param bool $redirect
17413 * @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.
17414 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
17415 *
17416 * @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.
17417 */
17418 function install_with_user(
17419 FS_User $user,
17420 $license_key = false,
17421 $trial_plan_id = false,
17422 $redirect = true,
17423 $setup_account = true,
17424 $sites = array()
17425 ) {
17426 // We have to set the user before getting user scope API handler.
17427 $this->_user = $user;
17428
17429 // Install the plugin.
17430 $result = $this->create_installs_with_user(
17431 $user,
17432 $license_key,
17433 $trial_plan_id,
17434 $sites,
17435 $redirect
17436 );
17437
17438 if ( ! $this->is_api_result_entity( $result ) &&
17439 ! $this->is_api_result_object( $result, 'installs' )
17440 ) {
17441 // @todo Handler potential API error of the $result
17442 }
17443
17444 if ( empty( $sites ) ) {
17445 $site = new FS_Site( $result );
17446 $this->_site = $site;
17447
17448 if ( ! $setup_account ) {
17449 $this->_store_site();
17450
17451 $this->sync_plan_if_not_exist( $site->plan_id );
17452
17453 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
17454 $this->sync_license_if_not_exist( $site->license_id, $license_key );
17455 }
17456
17457 $this->_admin_notices->remove_sticky( 'connect_account', false );
17458
17459 return $site;
17460 }
17461
17462 return $this->setup_account( $this->_user, $this->_site, $redirect );
17463 } else {
17464 $installs = array();
17465 foreach ( $result->installs as $install ) {
17466 $installs[] = new FS_Site( $install );
17467 }
17468
17469 return $this->setup_network_account(
17470 $user,
17471 $installs,
17472 $redirect
17473 );
17474 }
17475 }
17476
17477 /**
17478 * Initiate an API request to create a collection of installs.
17479 *
17480 * @author Vova Feldman (@svovaf)
17481 * @since 2.0.0
17482 *
17483 * @param \FS_User $user
17484 * @param bool $license_key
17485 * @param bool $trial_plan_id
17486 * @param array $sites
17487 * @param bool $redirect
17488 * @param bool $silent
17489 *
17490 * @return object|mixed
17491 */
17492 private function create_installs_with_user(
17493 FS_User $user,
17494 $license_key = false,
17495 $trial_plan_id = false,
17496 $sites = array(),
17497 $redirect = false,
17498 $silent = false
17499 ) {
17500 $extra_install_params = array(
17501 'uid' => $this->get_anonymous_id(),
17502 'is_disconnected' => false,
17503 );
17504
17505 if ( ! empty( $license_key ) ) {
17506 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
17507
17508 if ( $silent ) {
17509 $extra_install_params['ignore_license_owner'] = true;
17510 }
17511 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17512 $extra_install_params['trial_plan_id'] = $trial_plan_id;
17513 }
17514
17515 if ( ! empty( $sites ) ) {
17516 $extra_install_params['sites'] = $sites;
17517 }
17518
17519 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
17520
17521 // Install the plugin.
17522 $result = $this->get_api_user_scope_by_user( $user )->call(
17523 "/plugins/{$this->get_id()}/installs.json",
17524 'post',
17525 $args
17526 );
17527
17528 if ( ! $this->is_api_result_entity( $result ) &&
17529 ! $this->is_api_result_object( $result, 'installs' )
17530 ) {
17531 if ( ! empty( $args['license_key'] ) ) {
17532 // Pass the fully entered license key to the failure handler.
17533 $args['license_key'] = $license_key;
17534 }
17535
17536 $result = $this->apply_filters( 'after_install_failure', $result, $args );
17537
17538 if ( ! $silent ) {
17539 $this->_admin_notices->add(
17540 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17541 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
17542 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17543 'error'
17544 );
17545 }
17546
17547 if ( $redirect ) {
17548 /**
17549 * We set the user before getting the user scope API handler, so the user became temporarily
17550 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
17551 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
17552 * activation page based on the return value of `is_registered()`. In addition, in case the
17553 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
17554 * misleading plugin activation errors will be shown on the `Plugins` page.
17555 *
17556 * @author Leo Fajardo (@leorw)
17557 */
17558 $this->_user = null;
17559
17560 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
17561 }
17562 }
17563
17564 return $result;
17565 }
17566
17567 /**
17568 * Tries to activate add-on account based on parent plugin info.
17569 *
17570 * @author Vova Feldman (@svovaf)
17571 * @since 1.0.6
17572 *
17573 * @param Freemius $parent_fs
17574 * @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.
17575 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
17576 */
17577 private function _activate_addon_account(
17578 Freemius $parent_fs,
17579 $network_level_or_blog_id = null,
17580 FS_Plugin_License $bundle_license = null
17581 ) {
17582 if ( $this->is_registered() ) {
17583 // Already activated.
17584 return;
17585 }
17586
17587 /**
17588 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
17589 * already returns the data for the current blog.
17590 *
17591 * @author Leo Fajardo (@leorw)
17592 * @since 2.3.0
17593 */
17594 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
17595 array() :
17596 array( 'uid' => $this->get_anonymous_id() );
17597
17598 $params = $this->get_install_data_for_api(
17599 $uid_param_to_override,
17600 false,
17601 false,
17602 /**
17603 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
17604 * already includes the data for it.
17605 *
17606 * @author Leo Fajardo (@leorw)
17607 * @since 2.3.0
17608 */
17609 ( true !== $network_level_or_blog_id )
17610 );
17611
17612 if ( true === $network_level_or_blog_id ) {
17613 $params['sites'] = $this->get_sites_for_network_level_optin();
17614
17615 if ( empty( $params['sites'] ) ) {
17616 return;
17617 }
17618 }
17619
17620 if ( is_object( $bundle_license ) ) {
17621 $params['license_key'] = $bundle_license->secret_key;
17622 }
17623
17624 // Activate add-on with parent plugin credentials.
17625 $result = $parent_fs->get_api_site_scope()->call(
17626 "/addons/{$this->_plugin->id}/installs.json",
17627 'post',
17628 $params
17629 );
17630
17631 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17632 if ( is_object( $bundle_license ) ) {
17633 /**
17634 * 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.
17635 *
17636 * @author Leo Fajardo (@leorw)
17637 * @since 2.4.0
17638 */
17639 } else {
17640 $error_message = FS_Api::is_api_error_object( $result ) ?
17641 $result->error->message :
17642 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
17643
17644 $this->_admin_notices->add(
17645 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17646 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
17647 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17648 'error'
17649 );
17650 }
17651
17652 return;
17653 }
17654
17655 $addon_installs = $result->installs;
17656 foreach ( $addon_installs as $key => $addon_install ) {
17657 $addon_installs[ $key ] = new FS_Site( $addon_install );
17658 }
17659
17660 $first_install = $addon_installs[0];
17661
17662 // Get user information based on parent's plugin.
17663 $user = $parent_fs->get_user();
17664
17665 // First of all, set site and user info - otherwise we won't
17666 // be able to invoke API calls.
17667 $this->_site = $first_install;
17668 $this->_user = $user;
17669
17670 // Sync add-on plans.
17671 $this->_sync_plans();
17672
17673 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
17674
17675 // Get site's current plan.
17676 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
17677
17678 // Sync licenses.
17679 $this->_sync_licenses();
17680
17681 if ( ! fs_is_network_admin() ) {
17682 // Try to activate premium license.
17683 $this->_activate_license( true, $bundle_license );
17684
17685 if ( is_object( $bundle_license ) ) {
17686 $this->maybe_activate_bundle_license( $bundle_license );
17687 }
17688 } else {
17689 if ( is_object( $bundle_license ) ) {
17690 $premium_license = $bundle_license;
17691 } else {
17692 $license_id = fs_request_get( 'license_id' );
17693
17694 if ( is_object( $this->_site ) &&
17695 FS_Plugin_License::is_valid_id( $license_id ) &&
17696 $license_id == $this->_site->license_id
17697 ) {
17698 // License is already activated.
17699 return;
17700 }
17701
17702 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
17703 $this->_get_license_by_id( $license_id ) :
17704 $this->_get_available_premium_license();
17705 }
17706
17707 if ( is_object( $premium_license ) ) {
17708 $this->maybe_network_activate_addon_license( $premium_license );
17709 }
17710 }
17711 }
17712
17713 /**
17714 * @author Leo Fajardo (@leorw)
17715 * @since 2.3.0
17716 *
17717 * @param FS_Site[] $installs
17718 * @param bool $is_site_level
17719 */
17720 private function handle_account_connection( $installs, $is_site_level ) {
17721 $first_install = $installs[0];
17722
17723 if ( $is_site_level ) {
17724 $this->_set_account( $this->_user, $first_install );
17725
17726 $this->do_action( 'after_account_connection', $this->_user, $first_install );
17727 } else {
17728 $this->_store_user();
17729
17730 // Map site addresses to their blog IDs.
17731 $address_to_blog_map = $this->get_address_to_blog_map();
17732
17733 $first_blog_id = null;
17734 $blog_2_install_map = array();
17735 foreach ( $installs as $install ) {
17736 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
17737 $blog_id = $address_to_blog_map[ $address ];
17738
17739 $this->_store_site( true, $blog_id, $install );
17740
17741 if ( is_null( $first_blog_id ) ) {
17742 $first_blog_id = $blog_id;
17743 }
17744
17745 $blog_2_install_map[ $blog_id ] = $install;
17746 }
17747
17748 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
17749 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
17750 ) {
17751 // Store network user.
17752 $this->_storage->network_user_id = $this->_user->id;
17753 }
17754
17755 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
17756 $this->_storage->network_install_blog_id = $first_blog_id;
17757 }
17758
17759 if ( count( $installs ) === count( $address_to_blog_map ) ) {
17760 // Super admin opted in for all sites in the network.
17761 $this->_storage->is_network_connected = true;
17762 }
17763
17764 $this->_store_licenses( false );
17765
17766 self::$_accounts->store();
17767
17768 // Don't sync the installs data on network upgrade
17769 if ( ! $this->network_upgrade_mode_completed() ) {
17770 $this->send_installs_update();
17771 }
17772
17773 // Switch install context back to the first install.
17774 $this->_site = $first_install;
17775
17776 $current_blog = get_current_blog_id();
17777
17778 foreach ( $blog_2_install_map as $blog_id => $install ) {
17779 $this->switch_to_blog( $blog_id );
17780
17781 $this->do_action( 'after_account_connection', $this->_user, $install );
17782 }
17783
17784 $this->switch_to_blog( $current_blog );
17785
17786 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
17787 }
17788 }
17789
17790 /**
17791 * Tries to activate parent account based on add-on's info.
17792 *
17793 * @author Vova Feldman (@svovaf)
17794 * @since 1.2.2.7
17795 *
17796 * @param Freemius $parent_fs
17797 */
17798 private function activate_parent_account( Freemius $parent_fs ) {
17799 if ( ! $this->is_addon() ) {
17800 // This is not an add-on.
17801 return;
17802 }
17803
17804 if ( $parent_fs->is_registered() ) {
17805 // Already activated.
17806 return;
17807 }
17808
17809 // Activate parent with add-on's user credentials.
17810 $parent_install = $this->get_api_user_scope()->call(
17811 "/plugins/{$parent_fs->_plugin->id}/installs.json",
17812 'post',
17813 $parent_fs->get_install_data_for_api( array(
17814 'uid' => $parent_fs->get_anonymous_id(),
17815 ), false, false )
17816 );
17817
17818 if ( isset( $parent_install->error ) ) {
17819 $this->_admin_notices->add(
17820 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17821 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
17822 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17823 'error'
17824 );
17825
17826 return;
17827 }
17828
17829 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
17830
17831 if ( $parent_fs->is_pending_activation() ) {
17832 $parent_fs->_admin_notices->remove_sticky( 'activation_pending' );
17833
17834 unset( $parent_fs->_storage->is_pending_activation );
17835 }
17836
17837 // Get user information based on parent's plugin.
17838 $user = $this->get_user();
17839
17840 // First of all, set site info - otherwise we won't
17841 // be able to invoke API calls.
17842 $parent_fs->_site = new FS_Site( $parent_install );
17843 $parent_fs->_user = $user;
17844
17845 // Sync add-on plans.
17846 $parent_fs->_sync_plans();
17847
17848 $parent_fs->_set_account( $user, $parent_fs->_site );
17849 }
17850
17851 #endregion
17852
17853 #----------------------------------------------------------------------------------
17854 #region Admin Menu Items
17855 #----------------------------------------------------------------------------------
17856
17857 private $_menu_items = array();
17858
17859 /**
17860 * @author Vova Feldman (@svovaf)
17861 * @since 1.2.1.8
17862 *
17863 * @return array
17864 */
17865 function get_menu_items() {
17866 return $this->_menu_items;
17867 }
17868
17869 /**
17870 * @author Vova Feldman (@svovaf)
17871 * @since 1.0.7
17872 *
17873 * @return string
17874 */
17875 function get_menu_slug() {
17876 return $this->_menu->get_slug();
17877 }
17878
17879 /**
17880 * @author Vova Feldman (@svovaf)
17881 * @since 1.0.9
17882 */
17883 function _prepare_admin_menu() {
17884 // if ( ! $this->is_on() ) {
17885 // return;
17886 // }
17887
17888 /**
17889 * When running from a site admin with a network activated module and the connection
17890 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
17891 * site level settings.
17892 *
17893 * @author Vova Feldman (@svovaf)
17894 * @since 2.0.0
17895 */
17896 $should_hide_site_admin_settings = (
17897 $this->_is_network_active &&
17898 ! fs_is_network_admin() &&
17899 ! $this->is_delegated_connection() &&
17900 ! $this->is_anonymous() &&
17901 ! $this->is_registered()
17902 );
17903
17904 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
17905
17906 if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
17907 $should_hide_site_admin_settings
17908 ) {
17909 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
17910 } else {
17911 $this->do_action( fs_is_network_admin() ?
17912 'before_network_admin_menu_init' :
17913 'before_admin_menu_init'
17914 );
17915
17916 $this->add_menu_action();
17917
17918 $this->add_network_menu_when_missing();
17919
17920 $this->add_submenu_items();
17921 }
17922 }
17923
17924 /**
17925 * Admin dashboard menu items modifications.
17926 *
17927 * NOTE: admin_menu action executed before admin_init.
17928 *
17929 * @author Vova Feldman (@svovaf)
17930 * @since 1.0.7
17931 *
17932 */
17933 private function add_menu_action() {
17934 if ( $this->is_activation_mode() ) {
17935 if ( $this->show_opt_in_on_setting_page() ) {
17936 $this->override_plugin_menu_with_activation();
17937 } else {
17938 /**
17939 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
17940 */
17941 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
17942 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
17943 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
17944 fs_request_get_bool( 'pending_activation' )
17945 ) {
17946 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
17947 }
17948 }
17949 } else {
17950 if ( ! $this->is_registered() ) {
17951 // If not registered try to install user.
17952 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
17953 $this->_install_with_new_user();
17954 }
17955 } else if (
17956 fs_request_is_action( 'sync_user' ) &&
17957 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
17958 ) {
17959 $this->_handle_account_user_sync();
17960 }
17961 }
17962 }
17963
17964 /**
17965 * @author Vova Feldman (@svovaf)
17966 * @since 1.0.1
17967 */
17968 function _redirect_on_clicked_menu_link() {
17969 $this->_logger->entrance();
17970
17971 $page = fs_request_get('page');
17972 $page = is_string($page) ? strtolower($page) : '';
17973
17974 $this->_logger->log( 'page = ' . $page );
17975
17976 foreach ( $this->_menu_items as $priority => $items ) {
17977 foreach ( $items as $item ) {
17978 if ( isset( $item['url'] ) ) {
17979 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
17980 $this->_logger->log( 'Redirecting to ' . $item['url'] );
17981
17982 fs_redirect( $item['url'] );
17983 }
17984 }
17985 }
17986 }
17987 }
17988
17989 /**
17990 * Remove plugin's all admin menu items & pages, and replace with activation page.
17991 *
17992 * @author Vova Feldman (@svovaf)
17993 * @since 1.0.1
17994 */
17995 private function override_plugin_menu_with_activation() {
17996 $this->_logger->entrance();
17997
17998 $hook = false;
17999
18000 if ( ! $this->has_settings_menu() ) {
18001 // Add the opt-in page without a menu item.
18002 $hook = FS_Admin_Menu_Manager::add_subpage(
18003 null,
18004 $this->get_plugin_name(),
18005 $this->get_plugin_name(),
18006 'manage_options',
18007 $this->_slug,
18008 array( &$this, '_connect_page_render' )
18009 );
18010 } else if ( $this->_menu->is_top_level() ) {
18011 if ( $this->_menu->is_override_exact() ) {
18012 // Make sure the current page is matching the activation page.
18013 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18014 return;
18015 }
18016 }
18017
18018 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18019
18020 if ( false === $hook ) {
18021 // Create new menu item just for the opt-in.
18022 $hook = FS_Admin_Menu_Manager::add_page(
18023 $this->get_plugin_name(),
18024 $this->get_plugin_name(),
18025 'manage_options',
18026 $this->_menu->get_slug(),
18027 array( &$this, '_connect_page_render' )
18028 );
18029 }
18030 } else {
18031 $menus = array( $this->_menu->get_parent_slug() );
18032
18033 if ( $this->_menu->is_override_exact() ) {
18034 // Make sure the current page is matching the activation page.
18035 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18036 return;
18037 }
18038 }
18039
18040 foreach ( $menus as $parent_slug ) {
18041 $hook = $this->_menu->override_submenu_action(
18042 $parent_slug,
18043 $this->_menu->get_raw_slug(),
18044 array( &$this, '_connect_page_render' )
18045 );
18046
18047 if ( false !== $hook ) {
18048 // Found plugin's submenu item.
18049 break;
18050 }
18051 }
18052 }
18053
18054 if ( $this->is_activation_page() ) {
18055 // Clean admin page from distracting content.
18056 self::_clean_admin_content_section();
18057 }
18058
18059 if ( false !== $hook ) {
18060 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18061 $this->_install_with_current_user();
18062 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18063 $this->_install_with_new_user();
18064 }
18065 }
18066 }
18067
18068 /**
18069 * If a plugin was network activated and connected but don't have a network
18070 * level settings, then add an artificial menu item for the Account and other
18071 * Freemius settings.
18072 *
18073 * @author Vova Feldman (@svovaf)
18074 * @since 2.0.0
18075 */
18076 private function add_network_menu_when_missing() {
18077 $this->_logger->entrance();
18078
18079 if ( ! $this->_is_network_active ) {
18080 // Plugin wasn't activated on the network level.
18081 return;
18082 }
18083
18084 if ( ! fs_is_network_admin() ) {
18085 // The context is not the network admin.
18086 return;
18087 }
18088
18089 if ( $this->_menu->has_network_menu() ) {
18090 // Plugin already has a network level menu.
18091 return;
18092 }
18093
18094 if ( $this->is_network_activation_mode() ) {
18095 /**
18096 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18097 * screen is being shown.
18098 *
18099 * @author Leo Fajardo (@leorw)
18100 */
18101 return;
18102 }
18103
18104 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18105 if ( $this->is_network_delegated_connection() ) {
18106 // Super-admin delegated the connection to the site admins.
18107 return;
18108 }
18109 }
18110
18111 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18112
18113 if ( $this->_menu->has_menu() ||
18114 ! $this->is_addon() ||
18115 $this->is_activation_mode()
18116 ) {
18117 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18118 $this->get_plugin_name(),
18119 $this->get_plugin_name(),
18120 'manage_options',
18121 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18122 );
18123 }
18124 } else {
18125 $this->_menu->add_subpage_and_update(
18126 $this->_menu->get_parent_slug(),
18127 $this->get_plugin_name(),
18128 $this->get_plugin_name(),
18129 'manage_options',
18130 $this->_menu->get_slug()
18131 );
18132 }
18133 }
18134
18135 /**
18136 * @author Leo Fajardo (@leorw)
18137 * @since 1.2.1
18138 *
18139 * return string
18140 */
18141 function get_top_level_menu_capability() {
18142 global $menu;
18143
18144 $top_level_menu_slug = $this->get_top_level_menu_slug();
18145
18146 foreach ( $menu as $menu_info ) {
18147 /**
18148 * The second element in the menu info array is the capability/role that has access to the menu and the
18149 * third element is the menu slug.
18150 */
18151 if ( $menu_info[2] === $top_level_menu_slug ) {
18152 return $menu_info[1];
18153 }
18154 }
18155
18156 return 'read';
18157 }
18158
18159 /**
18160 * @author Vova Feldman (@svovaf)
18161 * @since 1.0.0
18162 *
18163 * @return string
18164 */
18165 private function get_top_level_menu_slug() {
18166 return ( $this->is_addon() ?
18167 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18168 $this->_menu->get_top_level_menu_slug() );
18169 }
18170
18171 /**
18172 * @author Vova Feldman (@svovaf)
18173 * @since 1.2.2.7
18174 *
18175 * @return string
18176 */
18177 function get_pricing_cta_label() {
18178 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18179
18180 if ( $this->is_in_trial_promotion() &&
18181 ! $this->is_paying_or_trial()
18182 ) {
18183 // If running a trial promotion, modify the pricing to load the trial.
18184 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18185 } else if ( $this->is_paying() ) {
18186 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18187 }
18188
18189 return $label;
18190 }
18191
18192 /**
18193 * @author Vova Feldman (@svovaf)
18194 * @since 1.2.2.7
18195 *
18196 * @return bool
18197 */
18198 function is_pricing_page_visible() {
18199 return (
18200 // Has at least one paid plan.
18201 $this->has_paid_plan() &&
18202 // Didn't ask to hide the pricing page.
18203 $this->is_page_visible( 'pricing' ) &&
18204 // Don't have a valid active license or has more than one plan.
18205 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18206 );
18207 }
18208
18209 /**
18210 * @author Leo Fajardo (@leorw)
18211 * @since 2.3.0
18212 *
18213 * @param bool $is_activation_mode
18214 *
18215 * @return bool
18216 */
18217 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18218 if ( $this->is_addon() ) {
18219 // No submenu items or action links for add-ons.
18220 return false;
18221 }
18222
18223 if ( $this->show_opt_in_on_themes_page() ) {
18224 if ( ! fs_is_network_admin() ) {
18225 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18226 return true;
18227 }
18228 } else if ( $is_activation_mode ) {
18229 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18230 return false;
18231 }
18232
18233 if ( fs_is_network_admin() ) {
18234 /**
18235 * Add submenu items or action links to network level when plugin was network activated and the super
18236 * admin did NOT delegate the connection of all sites to site admins.
18237 */
18238 return (
18239 $this->_is_network_active &&
18240 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18241 ! $this->is_network_delegated_connection() )
18242 );
18243 }
18244
18245 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18246 }
18247
18248 /**
18249 * Add default Freemius menu items.
18250 *
18251 * @author Vova Feldman (@svovaf)
18252 * @since 1.0.0
18253 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18254 */
18255 private function add_submenu_items() {
18256 $this->_logger->entrance();
18257
18258 $is_activation_mode = $this->is_activation_mode();
18259
18260 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18261
18262 if ( $add_submenu_items ) {
18263 if ( $this->has_affiliate_program() ) {
18264 // Add affiliation page.
18265 $this->add_submenu_item(
18266 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18267 array( &$this, '_affiliation_page_render' ),
18268 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18269 'manage_options',
18270 'affiliation',
18271 'Freemius::_clean_admin_content_section',
18272 WP_FS__DEFAULT_PRIORITY,
18273 $this->is_submenu_item_visible( 'affiliation' )
18274 );
18275 }
18276 }
18277
18278 if ( $add_submenu_items ||
18279 ( $is_activation_mode &&
18280 $this->is_only_premium() &&
18281 $this->is_admin_page( 'account' ) &&
18282 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18283 )
18284 ) {
18285 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18286 $show_account = (
18287 $this->is_submenu_item_visible( 'account' ) &&
18288 /**
18289 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18290 */
18291 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18292 );
18293
18294 // Add user account page.
18295 $this->add_submenu_item(
18296 $this->get_text_inline( 'Account', 'account' ),
18297 array( &$this, '_account_page_render' ),
18298 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18299 'manage_options',
18300 'account',
18301 array( &$this, '_account_page_load' ),
18302 WP_FS__DEFAULT_PRIORITY,
18303 ( $add_submenu_items && $show_account )
18304 );
18305 }
18306 }
18307
18308 if ( $add_submenu_items ) {
18309 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18310 // Add contact page.
18311 $this->add_submenu_item(
18312 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18313 array( &$this, '_contact_page_render' ),
18314 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18315 'manage_options',
18316 'contact',
18317 'Freemius::_clean_admin_content_section',
18318 WP_FS__DEFAULT_PRIORITY,
18319 $this->is_submenu_item_visible( 'contact' )
18320 );
18321 }
18322
18323 if ( $this->has_addons() ) {
18324 $this->add_submenu_item(
18325 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18326 array( &$this, '_addons_page_render' ),
18327 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18328 'manage_options',
18329 'addons',
18330 array( &$this, '_addons_page_load' ),
18331 WP_FS__LOWEST_PRIORITY - 1,
18332 $this->is_submenu_item_visible( 'addons' )
18333 );
18334 }
18335 }
18336
18337 if ( $add_submenu_items ||
18338 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18339 ) {
18340 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18341 $show_pricing = (
18342 $this->is_submenu_item_visible( 'pricing' ) &&
18343 $this->is_pricing_page_visible()
18344 );
18345
18346 $pricing_cta_text = $this->get_pricing_cta_label();
18347 $pricing_class = 'upgrade-mode';
18348 if ( $show_pricing ) {
18349 if ( $this->is_in_trial_promotion() &&
18350 ! $this->is_paying_or_trial()
18351 ) {
18352 // If running a trial promotion, modify the pricing to load the trial.
18353 $pricing_class = 'trial-mode';
18354 } else if ( $this->is_paying() ) {
18355 $pricing_class = '';
18356 }
18357 }
18358
18359 // Add upgrade/pricing page.
18360 $this->add_submenu_item(
18361 $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' ) ),
18362 array( &$this, '_pricing_page_render' ),
18363 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
18364 'manage_options',
18365 'pricing',
18366 'Freemius::_clean_admin_content_section',
18367 WP_FS__LOWEST_PRIORITY,
18368 ( $add_submenu_items && $show_pricing ),
18369 $pricing_class
18370 );
18371 }
18372 }
18373
18374 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
18375 /**
18376 * Add the other menu items if there are any when not in activation mode or license activation is not
18377 * required (license activation is required for registered or anonymous users after activating the
18378 * premium version when the site is not in trial mode or there's no active valid license).
18379 *
18380 * @author Leo Fajardo (@leorw)
18381 * @since 2.2.1
18382 */
18383 if ( 0 < count( $this->_menu_items ) ) {
18384 if ( ! $this->_menu->is_top_level() ) {
18385 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
18386
18387 // Append submenu items right after the plugin's submenu item.
18388 $this->order_sub_submenu_items();
18389 } else {
18390 // Append submenu items.
18391 $this->embed_submenu_items();
18392 }
18393 }
18394 }
18395 }
18396
18397 /**
18398 * Moved the actual submenu item additions to a separated function,
18399 * in order to support sub-submenu items when the plugin's settings
18400 * only have a submenu and not top-level menu item.
18401 *
18402 * @author Vova Feldman (@svovaf)
18403 * @since 1.1.4
18404 */
18405 private function embed_submenu_items() {
18406 $item_template = $this->_menu->is_top_level() ?
18407 '<span class="fs-submenu-item %s %s %s">%s</span>' :
18408 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
18409
18410 $top_level_menu_capability = $this->get_top_level_menu_capability();
18411
18412 ksort( $this->_menu_items );
18413
18414 $is_first_submenu_item = true;
18415
18416 foreach ( $this->_menu_items as $priority => $items ) {
18417 foreach ( $items as $item ) {
18418 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
18419
18420 $menu_item = sprintf(
18421 $item_template,
18422 $this->get_unique_affix(),
18423 $item['menu_slug'],
18424 ! empty( $item['class'] ) ? $item['class'] : '',
18425 $item['menu_title']
18426 );
18427
18428 $top_level_menu_slug = $this->get_top_level_menu_slug();
18429 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
18430
18431 if ( ! isset( $item['url'] ) ) {
18432 $hook = FS_Admin_Menu_Manager::add_subpage(
18433 $item['show_submenu'] ?
18434 $top_level_menu_slug :
18435 null,
18436 $item['page_title'],
18437 $menu_item,
18438 $capability,
18439 $menu_slug,
18440 $item['render_function']
18441 );
18442
18443 if ( false !== $item['before_render_function'] ) {
18444 add_action( "load-$hook", $item['before_render_function'] );
18445 }
18446 } else {
18447 FS_Admin_Menu_Manager::add_subpage(
18448 $item['show_submenu'] ?
18449 $top_level_menu_slug :
18450 null,
18451 $item['page_title'],
18452 $menu_item,
18453 $capability,
18454 $menu_slug,
18455 array( $this, '' )
18456 );
18457 }
18458
18459 if ( $item['show_submenu'] && $is_first_submenu_item ) {
18460 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
18461 /**
18462 * If the top-level menu has been dynamically created, remove the first submenu item that
18463 * WordPress automatically creates when there's no submenu item whose slug matches the
18464 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
18465 *
18466 * Awesome Plugin
18467 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
18468 *
18469 * @author Leo Fajardo (@leorw)
18470 */
18471 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
18472 }
18473
18474 $is_first_submenu_item = false;
18475 }
18476 }
18477 }
18478 }
18479
18480 /**
18481 * Re-order the submenu items so all Freemius added new submenu items
18482 * are added right after the plugin's settings submenu item.
18483 *
18484 * @author Vova Feldman (@svovaf)
18485 * @since 1.1.4
18486 */
18487 private function order_sub_submenu_items() {
18488 global $submenu;
18489
18490 $menu_slug = $this->_menu->get_top_level_menu_slug();
18491
18492 /**
18493 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
18494 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
18495 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
18496 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
18497 *
18498 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
18499 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
18500 * capability needed to access the parent menu as the capability for the submenus that we will add.
18501 */
18502 if ( empty( $submenu[ $menu_slug ] ) ) {
18503 return;
18504 }
18505
18506 $top_level_menu = &$submenu[ $menu_slug ];
18507
18508 $all_submenu_items_after = array();
18509
18510 $found_submenu_item = false;
18511
18512 foreach ( $top_level_menu as $submenu_id => $meta ) {
18513 if ( $found_submenu_item ) {
18514 // Remove all submenu items after the plugin's submenu item.
18515 $all_submenu_items_after[] = $meta;
18516 unset( $top_level_menu[ $submenu_id ] );
18517 }
18518
18519 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
18520 // Found the submenu item, put all below.
18521 $found_submenu_item = true;
18522 continue;
18523 }
18524 }
18525
18526 // Embed all plugin's new submenu items.
18527 $this->embed_submenu_items();
18528
18529 // Start with specially high number to make sure it's appended.
18530 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
18531 foreach ( $all_submenu_items_after as $meta ) {
18532 $top_level_menu[ $i ] = $meta;
18533 $i ++;
18534 }
18535
18536 // Sort submenu items.
18537 ksort( $top_level_menu );
18538 }
18539
18540 /**
18541 * Helper method to return the module's support forum URL.
18542 *
18543 * @author Vova Feldman (@svovaf)
18544 * @since 1.2.2.7
18545 *
18546 * @return string
18547 */
18548 function get_support_forum_url() {
18549 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
18550 }
18551
18552 /**
18553 * Displays the Support Forum link when enabled.
18554 *
18555 * Can be filtered like so:
18556 *
18557 * function _fs_show_support_menu( $is_visible, $menu_id ) {
18558 * if ( 'support' === $menu_id ) {
18559 * return _fs->is_registered();
18560 * }
18561 * return $is_visible;
18562 * }
18563 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
18564 *
18565 */
18566 function _add_default_submenu_items() {
18567 if ( ! $this->is_on() ) {
18568 return;
18569 }
18570
18571 if ( ! $this->is_activation_mode() &&
18572 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
18573 ( ! $this->_is_network_active && is_admin() ) )
18574 ) {
18575 $this->add_submenu_link_item(
18576 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
18577 $this->get_support_forum_url(),
18578 'wp-support-forum',
18579 null,
18580 50,
18581 $this->is_submenu_item_visible( 'support' )
18582 );
18583 }
18584 }
18585
18586 /**
18587 * @author Vova Feldman (@svovaf)
18588 * @since 1.0.1
18589 *
18590 * @param string $menu_title
18591 * @param callable $render_function
18592 * @param bool|string $page_title
18593 * @param string $capability
18594 * @param bool|string $menu_slug
18595 * @param bool|callable $before_render_function
18596 * @param int $priority
18597 * @param bool $show_submenu
18598 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
18599 */
18600 function add_submenu_item(
18601 $menu_title,
18602 $render_function,
18603 $page_title = false,
18604 $capability = 'manage_options',
18605 $menu_slug = false,
18606 $before_render_function = false,
18607 $priority = WP_FS__DEFAULT_PRIORITY,
18608 $show_submenu = true,
18609 $class = ''
18610 ) {
18611 $this->_logger->entrance( 'Title = ' . $menu_title );
18612
18613 if ( $this->is_addon() ) {
18614 $parent_fs = $this->get_parent_instance();
18615
18616 if ( is_object( $parent_fs ) ) {
18617 $parent_fs->add_submenu_item(
18618 $menu_title,
18619 $render_function,
18620 $page_title,
18621 $capability,
18622 $menu_slug,
18623 $before_render_function,
18624 $priority,
18625 $show_submenu,
18626 $class
18627 );
18628
18629 return;
18630 }
18631 }
18632
18633 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
18634 $this->_menu_items[ $priority ] = array();
18635 }
18636
18637 $this->_menu_items[ $priority ][] = array(
18638 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
18639 'menu_title' => $menu_title,
18640 'capability' => $capability,
18641 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
18642 'render_function' => $render_function,
18643 'before_render_function' => $before_render_function,
18644 'show_submenu' => $show_submenu,
18645 'class' => $class,
18646 );
18647 }
18648
18649 /**
18650 * @author Vova Feldman (@svovaf)
18651 * @since 1.0.1
18652 *
18653 * @param string $menu_title
18654 * @param string $url
18655 * @param bool $menu_slug
18656 * @param string $capability
18657 * @param int $priority
18658 * @param bool $show_submenu
18659 */
18660 function add_submenu_link_item(
18661 $menu_title,
18662 $url,
18663 $menu_slug = false,
18664 $capability = 'read',
18665 $priority = WP_FS__DEFAULT_PRIORITY,
18666 $show_submenu = true
18667 ) {
18668 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
18669
18670 if ( $this->is_addon() ) {
18671 $parent_fs = $this->get_parent_instance();
18672
18673 if ( is_object( $parent_fs ) ) {
18674 $parent_fs->add_submenu_link_item(
18675 $menu_title,
18676 $url,
18677 $menu_slug,
18678 $capability,
18679 $priority,
18680 $show_submenu
18681 );
18682
18683 return;
18684 }
18685 }
18686
18687 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
18688 $this->_menu_items[ $priority ] = array();
18689 }
18690
18691 $this->_menu_items[ $priority ][] = array(
18692 'menu_title' => $menu_title,
18693 'capability' => $capability,
18694 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
18695 'url' => $url,
18696 'page_title' => $menu_title,
18697 'render_function' => 'fs_dummy',
18698 'before_render_function' => '',
18699 'show_submenu' => $show_submenu,
18700 );
18701 }
18702
18703 #endregion ------------------------------------------------------------------
18704
18705 #--------------------------------------------------------------------------------
18706 #region Admin Notices
18707 #--------------------------------------------------------------------------------
18708
18709 /**
18710 * @author Vova Feldman (@svovaf)
18711 * @since 2.3.1
18712 *
18713 * @param string|string[] $ids
18714 * @param int|null $network_level_or_blog_id
18715 *
18716 * @uses FS_Admin_Notices::remove_sticky()
18717 */
18718 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
18719 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
18720 }
18721
18722 #endregion
18723
18724 #--------------------------------------------------------------------------------
18725 #region Actions / Hooks / Filters
18726 #--------------------------------------------------------------------------------
18727
18728 /**
18729 * @author Vova Feldman (@svovaf)
18730 * @since 1.1.7
18731 *
18732 * @param string $tag
18733 *
18734 * @return string
18735 */
18736 public function get_action_tag( $tag ) {
18737 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
18738 }
18739
18740 /**
18741 * @author Vova Feldman (@svovaf)
18742 * @since 1.2.1.6
18743 *
18744 * @param string $tag
18745 * @param string $slug
18746 * @param bool $is_plugin
18747 *
18748 * @return string
18749 */
18750 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
18751 $action = "fs_{$tag}";
18752
18753 if ( ! empty( $slug ) ) {
18754 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
18755 }
18756
18757 return $action;
18758 }
18759
18760 /**
18761 * Returns a string that can be used to generate a unique action name,
18762 * option name, HTML element ID, or HTML element class.
18763 *
18764 * @author Leo Fajardo (@leorw)
18765 * @since 1.2.2
18766 *
18767 * @return string
18768 */
18769 public function get_unique_affix() {
18770 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
18771 }
18772
18773 /**
18774 * Returns a string that can be used to generate a unique action name,
18775 * option name, HTML element ID, or HTML element class.
18776 *
18777 * @author Vova Feldman (@svovaf)
18778 * @since 1.2.2.5
18779 *
18780 * @param string $slug
18781 * @param bool $is_plugin
18782 *
18783 * @return string
18784 */
18785 static function get_module_unique_affix( $slug, $is_plugin = true ) {
18786 $affix = $slug;
18787
18788 if ( ! $is_plugin ) {
18789 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
18790 }
18791
18792 return $affix;
18793 }
18794
18795 /**
18796 * @author Vova Feldman (@svovaf)
18797 * @since 1.2.1
18798 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
18799 * based on the slug for backward compatibility.
18800 *
18801 * @param string $tag
18802 *
18803 * @return string
18804 */
18805 function get_ajax_action( $tag ) {
18806 return self::get_ajax_action_static( $tag, $this->_module_id );
18807 }
18808
18809 /**
18810 * @author Vova Feldman (@svovaf)
18811 * @since 1.2.1.7
18812 *
18813 * @param string $tag
18814 *
18815 * @return string
18816 */
18817 function get_ajax_security( $tag ) {
18818 return wp_create_nonce( $this->get_ajax_action( $tag ) );
18819 }
18820
18821 /**
18822 * @author Vova Feldman (@svovaf)
18823 * @since 1.2.1.7
18824 *
18825 * @param string $tag
18826 */
18827 function check_ajax_referer( $tag ) {
18828 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
18829 }
18830
18831 /**
18832 * @author Vova Feldman (@svovaf)
18833 * @since 1.2.1.6
18834 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
18835 * based on the slug for backward compatibility.
18836 *
18837 * @param string $tag
18838 * @param number|null $module_id
18839 *
18840 * @return string
18841 */
18842 private static function get_ajax_action_static( $tag, $module_id = null ) {
18843 $action = "fs_{$tag}";
18844
18845 if ( ! empty( $module_id ) ) {
18846 $action .= "_{$module_id}";
18847 }
18848
18849 return $action;
18850 }
18851
18852 /**
18853 * Do action, specific for the current context plugin.
18854 *
18855 * @author Vova Feldman (@svovaf)
18856 * @since 1.0.1
18857 *
18858 * @param string $tag The name of the action to be executed.
18859 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
18860 * functions hooked to the action. Default empty.
18861 *
18862 * @uses do_action()
18863 */
18864 function do_action( $tag, $arg = '' ) {
18865 $this->_logger->entrance( $tag );
18866
18867 $args = func_get_args();
18868
18869 call_user_func_array( 'do_action', array_merge(
18870 array( $this->get_action_tag( $tag ) ),
18871 array_slice( $args, 1 ) )
18872 );
18873 }
18874
18875 /**
18876 * Add action, specific for the current context plugin.
18877 *
18878 * @author Vova Feldman (@svovaf)
18879 * @since 1.0.1
18880 *
18881 * @param string $tag
18882 * @param callable $function_to_add
18883 * @param int $priority
18884 * @param int $accepted_args
18885 *
18886 * @uses add_action()
18887 */
18888 function add_action(
18889 $tag,
18890 $function_to_add,
18891 $priority = WP_FS__DEFAULT_PRIORITY,
18892 $accepted_args = 1
18893 ) {
18894 $this->_logger->entrance( $tag );
18895
18896 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
18897 }
18898
18899 /**
18900 * Add AJAX action, specific for the current context plugin.
18901 *
18902 * @author Vova Feldman (@svovaf)
18903 * @since 1.2.1
18904 *
18905 * @param string $tag
18906 * @param callable $function_to_add
18907 * @param int $priority
18908 *
18909 * @uses add_action()
18910 *
18911 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
18912 */
18913 function add_ajax_action(
18914 $tag,
18915 $function_to_add,
18916 $priority = WP_FS__DEFAULT_PRIORITY
18917 ) {
18918 $this->_logger->entrance( $tag );
18919
18920 return self::add_ajax_action_static(
18921 $tag,
18922 $function_to_add,
18923 $priority,
18924 $this->_module_id
18925 );
18926 }
18927
18928 /**
18929 * Add AJAX action.
18930 *
18931 * @author Vova Feldman (@svovaf)
18932 * @since 1.2.1.6
18933 *
18934 * @param string $tag
18935 * @param callable $function_to_add
18936 * @param int $priority
18937 * @param number|null $module_id
18938 *
18939 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
18940 * @uses add_action()
18941 *
18942 */
18943 static function add_ajax_action_static(
18944 $tag,
18945 $function_to_add,
18946 $priority = WP_FS__DEFAULT_PRIORITY,
18947 $module_id = null
18948 ) {
18949 self::$_static_logger->entrance( $tag );
18950
18951 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
18952 return false;
18953 }
18954
18955 add_action(
18956 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
18957 $function_to_add,
18958 $priority,
18959 0
18960 );
18961
18962 self::$_static_logger->info( "$tag AJAX callback action added." );
18963
18964 return true;
18965 }
18966
18967 /**
18968 * Send a JSON response back to an Ajax request.
18969 *
18970 * @author Vova Feldman (@svovaf)
18971 * @since 1.2.1.5
18972 *
18973 * @param mixed $response
18974 */
18975 static function shoot_ajax_response( $response ) {
18976 wp_send_json( $response );
18977 }
18978
18979 /**
18980 * Send a JSON response back to an Ajax request, indicating success.
18981 *
18982 * @author Vova Feldman (@svovaf)
18983 * @since 1.2.1.5
18984 *
18985 * @param mixed $data Data to encode as JSON, then print and exit.
18986 */
18987 static function shoot_ajax_success( $data = null ) {
18988 wp_send_json_success( $data );
18989 }
18990
18991 /**
18992 * Send a JSON response back to an Ajax request, indicating failure.
18993 *
18994 * @author Vova Feldman (@svovaf)
18995 * @since 1.2.1.5
18996 *
18997 * @param mixed $error Optional error message.
18998 */
18999 static function shoot_ajax_failure( $error = '' ) {
19000 $result = array( 'success' => false );
19001 if ( ! empty( $error ) ) {
19002 $result['error'] = $error;
19003 }
19004
19005 wp_send_json( $result );
19006 }
19007
19008 /**
19009 * Apply filter, specific for the current context plugin.
19010 *
19011 * @author Vova Feldman (@svovaf)
19012 * @since 1.0.9
19013 *
19014 * @param string $tag The name of the filter hook.
19015 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19016 *
19017 * @return mixed The filtered value after all hooked functions are applied to it.
19018 *
19019 * @uses apply_filters()
19020 */
19021 function apply_filters( $tag, $value ) {
19022 $this->_logger->entrance( $tag );
19023
19024 $args = func_get_args();
19025 array_unshift( $args, $this->get_unique_affix() );
19026
19027 return call_user_func_array( 'fs_apply_filter', $args );
19028 }
19029
19030 /**
19031 * Add filter, specific for the current context plugin.
19032 *
19033 * @author Vova Feldman (@svovaf)
19034 * @since 1.0.9
19035 *
19036 * @param string $tag
19037 * @param callable $function_to_add
19038 * @param int $priority
19039 * @param int $accepted_args
19040 *
19041 * @uses add_filter()
19042 */
19043 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19044 $this->_logger->entrance( $tag );
19045
19046 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19047 }
19048
19049 /**
19050 * Check if has filter.
19051 *
19052 * @author Vova Feldman (@svovaf)
19053 * @since 1.1.4
19054 *
19055 * @param string $tag
19056 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19057 *
19058 * @return false|int
19059 *
19060 * @uses has_filter()
19061 */
19062 function has_filter( $tag, $function_to_check = false ) {
19063 $this->_logger->entrance( $tag );
19064
19065 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19066 }
19067
19068 #endregion
19069
19070 /**
19071 * Override default i18n text phrases.
19072 *
19073 * @author Vova Feldman (@svovaf)
19074 * @since 1.1.6
19075 *
19076 * @param string[] string $key_value
19077 *
19078 * @uses fs_override_i18n()
19079 */
19080 function override_i18n( $key_value ) {
19081 fs_override_i18n( $key_value, $this->_slug );
19082 }
19083
19084 /* Account Page
19085 ------------------------------------------------------------------------------------------------------------------*/
19086 /**
19087 * Update site information.
19088 *
19089 * @author Vova Feldman (@svovaf)
19090 * @since 1.0.1
19091 *
19092 * @param bool $store Flush to Database if true.
19093 * @param null|int $network_level_or_blog_id Since 2.0.0
19094 * @param \FS_Site $site Since 2.0.0
19095 */
19096 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null ) {
19097 $this->_logger->entrance();
19098
19099 if ( is_null( $site ) ) {
19100 $site = $this->_site;
19101 }
19102
19103 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19104 $this->_logger->error( "Empty install ID, can't store site." );
19105
19106 return;
19107 }
19108
19109 $site_clone = clone $site;
19110
19111 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id );
19112
19113 if ( is_object( $this->_user ) && $this->_user->id != $site->user_id ) {
19114 $this->sync_user_by_current_install( $site->user_id );
19115
19116 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19117
19118 if ( empty( $prev_stored_user_id ) &&
19119 is_object($this->_user) && $this->_user->id != $site->user_id
19120 ) {
19121 /**
19122 * Store the current user ID as the previous user ID so that the previous user can be used
19123 * as the install's owner while the new owner's details are not yet available.
19124 *
19125 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19126 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19127 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19128 * into `replica`.
19129 *
19130 * @author Leo Fajardo (@leorw)
19131 */
19132 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19133 }
19134 }
19135
19136 $sites[ $this->_slug ] = $site_clone;
19137
19138 $this->set_account_option( 'sites', $sites, $store, $network_level_or_blog_id );
19139 }
19140
19141 /**
19142 * Update plugin's plans information.
19143 *
19144 * @author Vova Feldman (@svovaf)
19145 * @since 1.0.2
19146 *
19147 * @param bool $store Flush to Database if true.
19148 */
19149 private function _store_plans( $store = true ) {
19150 $this->_logger->entrance();
19151
19152 $plans = self::get_all_plans( $this->_module_type );
19153
19154 // Copy plans.
19155 $encrypted_plans = array();
19156 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19157 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19158 }
19159
19160 $plans[ $this->_slug ] = $encrypted_plans;
19161
19162 $this->set_account_option( 'plans', $plans, $store );
19163 }
19164
19165 /**
19166 * Update user's plugin licenses.
19167 *
19168 * @author Vova Feldman (@svovaf)
19169 * @since 1.0.5
19170 *
19171 * @param bool $store
19172 * @param number|bool $module_id
19173 * @param FS_Plugin_License[] $licenses
19174 */
19175 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19176 $this->_logger->entrance();
19177
19178 $all_licenses = self::get_all_licenses();
19179
19180 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19181 $module_id = $this->_module_id;
19182
19183 $user_licenses = is_array( $this->_licenses ) ?
19184 $this->_licenses :
19185 array();
19186
19187 if ( empty( $user_licenses ) ) {
19188 // If the context user doesn't have any license, don't update the licenses collection.
19189 return;
19190 }
19191
19192 $new_user_licenses_map = array();
19193 foreach ( $user_licenses as $user_license ) {
19194 $new_user_licenses_map[ $user_license->id ] = $user_license;
19195 }
19196
19197 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19198
19199 // Update user licenses.
19200 $licenses_to_update_count = count( $new_user_licenses_map );
19201 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19202 if ( 0 === $licenses_to_update_count ) {
19203 break;
19204 }
19205
19206 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19207 // Update license.
19208 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19209 unset( $new_user_licenses_map[ $license->id ] );
19210
19211 $licenses_to_update_count --;
19212 }
19213 }
19214
19215 if ( ! empty( $new_user_licenses_map ) ) {
19216 // Add new licenses.
19217 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19218 }
19219
19220 $licenses = $all_licenses[ $module_id ];
19221 }
19222
19223 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19224 $all_licenses[ $module_id ] = array();
19225 }
19226
19227 $all_licenses[ $module_id ] = $licenses;
19228
19229 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19230 }
19231
19232 /**
19233 * Update user information.
19234 *
19235 * @author Vova Feldman (@svovaf)
19236 * @since 1.0.1
19237 *
19238 * @param bool $store Flush to Database if true.
19239 */
19240 private function _store_user( $store = true ) {
19241 $this->_logger->entrance();
19242
19243 if ( empty( $this->_user->id ) ) {
19244 $this->_logger->error( "Empty user ID, can't store user." );
19245
19246 return;
19247 }
19248
19249 $users = self::get_all_users();
19250 $users[ $this->_user->id ] = $this->_user;
19251 self::$_accounts->set_option( 'users', $users, $store );
19252 }
19253
19254 /**
19255 * Update new updates information.
19256 *
19257 * @author Vova Feldman (@svovaf)
19258 * @since 1.0.4
19259 *
19260 * @param FS_Plugin_Tag|null $update
19261 * @param bool $store Flush to Database if true.
19262 * @param bool|number $plugin_id
19263 */
19264 private function _store_update( $update, $store = true, $plugin_id = false ) {
19265 $this->_logger->entrance();
19266
19267 if ( $update instanceof FS_Plugin_Tag ) {
19268 $update->updated = time();
19269 }
19270
19271 if ( ! is_numeric( $plugin_id ) ) {
19272 $plugin_id = $this->_plugin->id;
19273 }
19274
19275 $updates = self::get_all_updates();
19276 $updates[ $plugin_id ] = $update;
19277 self::$_accounts->set_option( 'updates', $updates, $store );
19278 }
19279
19280 /**
19281 * Update new updates information.
19282 *
19283 * @author Vova Feldman (@svovaf)
19284 * @since 1.0.6
19285 *
19286 * @param FS_Plugin[] $plugin_addons
19287 * @param bool $store Flush to Database if true.
19288 */
19289 private function _store_addons( $plugin_addons, $store = true ) {
19290 $this->_logger->entrance();
19291
19292 $addons = self::get_all_addons();
19293 $addons[ $this->_plugin->id ] = $plugin_addons;
19294 self::$_accounts->set_option( 'addons', $addons, $store );
19295 }
19296
19297 /**
19298 * Delete plugin's associated add-ons.
19299 *
19300 * @author Vova Feldman (@svovaf)
19301 * @since 1.0.8
19302 *
19303 * @param bool $store
19304 *
19305 * @return bool
19306 */
19307 private function _delete_account_addons( $store = true ) {
19308 $all_addons = self::get_all_account_addons();
19309
19310 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
19311 return false;
19312 }
19313
19314 unset( $all_addons[ $this->_plugin->id ] );
19315
19316 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19317
19318 return true;
19319 }
19320
19321 /**
19322 * Update account add-ons list.
19323 *
19324 * @author Vova Feldman (@svovaf)
19325 * @since 1.0.6
19326 *
19327 * @param FS_Plugin[] $addons
19328 * @param bool $store Flush to Database if true.
19329 */
19330 private function _store_account_addons( $addons, $store = true ) {
19331 $this->_logger->entrance();
19332
19333 $all_addons = self::get_all_account_addons();
19334 $all_addons[ $this->_plugin->id ] = $addons;
19335 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19336 }
19337
19338 /**
19339 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
19340 * the valid user licenses will be fetched again and the account add-ons may be updated.
19341 *
19342 * @author Leo Fajardo (@leorw)
19343 * @since 2.2.4
19344 */
19345 private function purge_valid_user_licenses_cache() {
19346 if ( ! $this->is_registered() ) {
19347 return;
19348 }
19349
19350 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
19351 }
19352
19353 /**
19354 * @author Leo Fajardo (@leorw)
19355 * @since 2.3.0
19356 *
19357 * @param array $all_licenses
19358 * @param number|null $site_license_id
19359 * @param bool $include_parent_licenses
19360 *
19361 * @return array
19362 */
19363 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
19364 $foreign_licenses = array(
19365 'ids' => array(),
19366 'license_keys' => array()
19367 );
19368
19369 $parent_license_ids_map = array();
19370
19371 foreach ( $all_licenses as $license ) {
19372 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
19373 continue;
19374 }
19375
19376 $foreign_licenses['ids'][] = $license->id;
19377 $foreign_licenses['license_keys'][] = $license->secret_key;
19378
19379 if (
19380 $include_parent_licenses &&
19381 is_object( $this->_license ) &&
19382 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
19383 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
19384 ) {
19385 /**
19386 * Include the parent license's info only if it has not been included before since child licenses
19387 * can have the same parent license.
19388 */
19389 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
19390 $foreign_licenses['license_keys'][] = $license->secret_key;
19391
19392 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
19393 }
19394 }
19395
19396 if ( empty( $foreign_licenses['ids'] ) ) {
19397 $foreign_licenses = array();
19398 }
19399
19400 return $foreign_licenses;
19401 }
19402
19403 /**
19404 * @author Leo Fajardo (@leorw)
19405 * @since 2.3.0
19406 *
19407 * @return string
19408 */
19409 private function get_valid_user_licenses_endpoint() {
19410 $user_licenses_endpoint = '/licenses.json?type=active' .
19411 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
19412
19413 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
19414
19415 if ( ! empty ( $foreign_licenses ) ) {
19416 $foreign_licenses = array(
19417 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
19418 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
19419 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
19420 );
19421
19422 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
19423 }
19424
19425 return $user_licenses_endpoint;
19426 }
19427
19428 /**
19429 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
19430 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
19431 * method filters out non–add-on product IDs and stores the add-on IDs.
19432 *
19433 * @author Leo Fajardo (@leorw)
19434 * @since 2.2.4
19435 *
19436 * @return stdClass[] array
19437 */
19438 private function fetch_valid_user_licenses() {
19439 $this->_logger->entrance();
19440
19441 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
19442
19443 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
19444 ! is_array( $result->licenses )
19445 ) {
19446 return array();
19447 }
19448
19449 return $result->licenses;
19450 }
19451
19452 /**
19453 * @author Leo Fajardo (@leorw)
19454 * @since 2.2.4
19455 *
19456 * @return number[] Account add-on IDs.
19457 */
19458 function get_updated_account_addons() {
19459 $addons = $this->get_addons();
19460 if ( empty( $addons ) ) {
19461 return array();
19462 }
19463
19464 $account_addons = $this->get_account_addons();
19465 if ( ! is_array( $account_addons ) ) {
19466 $account_addons = array();
19467 }
19468
19469 $user_licenses = $this->is_registered() ?
19470 $this->fetch_valid_user_licenses() :
19471 array();
19472
19473 if ( empty( $user_licenses ) ) {
19474 return $account_addons;
19475 }
19476
19477 $addon_ids = array();
19478 foreach ( $addons as $addon ) {
19479 $addon_ids[] = $addon->id;
19480 }
19481
19482 $license_product_ids = array();
19483
19484 foreach ( $user_licenses as $license ) {
19485 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
19486 $license_product_ids = array_merge( $license_product_ids, $license->products );
19487 } else {
19488 $license_product_ids[] = $license->plugin_id;
19489 }
19490 }
19491
19492 // Filter out non–add-on IDs.
19493 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
19494 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
19495 $this->_store_account_addons( array_unique( $new_account_addons ) );
19496 }
19497
19498 return $new_account_addons;
19499 }
19500
19501 /**
19502 * Store account params in the Database.
19503 *
19504 * @author Vova Feldman (@svovaf)
19505 * @since 1.0.1
19506 *
19507 * @param null|int $blog_id Since 2.0.0
19508 */
19509 private function _store_account( $blog_id = null ) {
19510 $this->_logger->entrance();
19511
19512 $this->_store_site( false, $blog_id );
19513 $this->_store_user( false );
19514 $this->_store_plans( false );
19515 $this->_store_licenses( false );
19516
19517 self::$_accounts->store( $blog_id );
19518 }
19519
19520 /**
19521 * Sync user's information.
19522 *
19523 * @author Vova Feldman (@svovaf)
19524 * @since 1.0.3
19525 * @uses FS_Api
19526 */
19527 private function _handle_account_user_sync() {
19528 $this->_logger->entrance();
19529
19530 $api = $this->get_api_user_scope();
19531
19532 // Get user's information.
19533 $user = $api->get( '/', true );
19534
19535 if ( isset( $user->id ) ) {
19536 $this->_user->first = $user->first;
19537 $this->_user->last = $user->last;
19538 $this->_user->email = $user->email;
19539
19540 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
19541
19542 if ( $user->is_verified &&
19543 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
19544 ) {
19545 $this->_user->is_verified = true;
19546
19547 $this->do_action( 'account_email_verified', $user->email );
19548
19549 $this->_admin_notices->add(
19550 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
19551 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
19552 'success',
19553 // Make admin sticky if account menu item is invisible,
19554 // since the page will be auto redirected to the plugin's
19555 // main settings page, and the non-sticky message
19556 // will disappear.
19557 ! $is_menu_item_account_visible,
19558 'email_verified'
19559 );
19560 }
19561
19562 // Flush user details to DB.
19563 $this->_store_user();
19564
19565 $this->do_action( 'after_account_user_sync', $user );
19566
19567 /**
19568 * If account menu item is hidden, redirect to plugin's main settings page.
19569 *
19570 * @author Vova Feldman (@svovaf)
19571 * @since 1.1.6
19572 *
19573 * @link https://github.com/Freemius/wordpress-sdk/issues/6
19574 */
19575 if ( ! $is_menu_item_account_visible ) {
19576 fs_redirect( $this->_get_admin_page_url() );
19577 }
19578 }
19579 }
19580
19581 /**
19582 * @author Vova Feldman (@svovaf)
19583 * @since 1.0.9
19584 * @uses FS_Api
19585 *
19586 * @param number|bool $license_id
19587 *
19588 * @return FS_Subscription|object|bool
19589 */
19590 private function _fetch_site_license_subscription( $license_id = false ) {
19591 $this->_logger->entrance();
19592 $api = $this->get_api_site_scope();
19593
19594 if ( ! is_numeric( $license_id ) ) {
19595 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
19596 $this->_license->parent_license_id :
19597 $this->_license->id;
19598 }
19599
19600 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
19601
19602 return ! isset( $result->error ) ?
19603 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
19604 new FS_Subscription( $result->subscriptions[0] ) :
19605 false
19606 ) :
19607 $result;
19608 }
19609
19610 /**
19611 * @author Vova Feldman (@svovaf)
19612 * @since 1.0.4
19613 * @uses FS_Api
19614 *
19615 * @param number|bool $plan_id
19616 *
19617 * @return FS_Plugin_Plan|object
19618 */
19619 private function _fetch_site_plan( $plan_id = false ) {
19620 $this->_logger->entrance();
19621 $api = $this->get_api_site_scope();
19622
19623 if ( ! is_numeric( $plan_id ) ) {
19624 $plan_id = $this->_site->plan_id;
19625 }
19626
19627 $plan = $api->get( "/plans/{$plan_id}.json", true );
19628
19629 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
19630 }
19631
19632 /**
19633 * @author Vova Feldman (@svovaf)
19634 * @since 1.0.5
19635 * @uses FS_Api
19636 *
19637 * @return FS_Plugin_Plan[]|object
19638 */
19639 private function _fetch_plugin_plans() {
19640 $this->_logger->entrance();
19641 $api = $this->get_current_or_network_user_api_scope();
19642
19643 /**
19644 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
19645 */
19646 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
19647
19648 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
19649 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
19650 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
19651 }
19652
19653 $result = $result->plans;
19654 }
19655
19656 return $result;
19657 }
19658
19659 /**
19660 * @author Vova Feldman (@svovaf)
19661 * @since 2.0.0
19662 *
19663 * @param number $plan_id
19664 *
19665 * @return \FS_Plugin_Plan|object
19666 */
19667 private function fetch_plan_by_id( $plan_id ) {
19668 $this->_logger->entrance();
19669 $api = $this->get_current_or_network_user_api_scope();
19670
19671 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
19672
19673 return $this->is_api_result_entity( $result ) ?
19674 new FS_Plugin_Plan( $result ) :
19675 $result;
19676 }
19677
19678 /**
19679 * @author Vova Feldman (@svovaf)
19680 * @since 1.0.5
19681 * @uses FS_Api
19682 *
19683 * @param number|bool $plugin_id
19684 * @param number|bool $site_license_id
19685 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
19686 * @param number|null $blog_id
19687 *
19688 * @return FS_Plugin_License[]|object
19689 */
19690 private function _fetch_licenses(
19691 $plugin_id = false,
19692 $site_license_id = false,
19693 $foreign_licenses = array(),
19694 $blog_id = null
19695 ) {
19696 $this->_logger->entrance();
19697
19698 $api = $this->get_api_user_scope();
19699
19700 if ( ! is_numeric( $plugin_id ) ) {
19701 $plugin_id = $this->_plugin->id;
19702 }
19703
19704 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
19705 if ( ! empty ( $foreign_licenses ) ) {
19706 $foreign_licenses = array(
19707 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
19708 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
19709 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
19710 );
19711
19712 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
19713 }
19714
19715 $result = $api->get( $user_licenses_endpoint, true );
19716
19717 $is_site_license_synced = false;
19718
19719 $api_errors = array();
19720
19721 if ( $this->is_api_result_object( $result, 'licenses' ) &&
19722 is_array( $result->licenses )
19723 ) {
19724 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
19725 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
19726
19727 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
19728 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
19729 }
19730 }
19731
19732 $result = $result->licenses;
19733 } else {
19734 $api_errors[] = $result;
19735 $result = array();
19736 }
19737
19738 if ( ! $is_site_license_synced ) {
19739 if ( ! is_null( $blog_id ) ) {
19740 /**
19741 * If blog ID is not null, the request is for syncing of the license of a single site via the
19742 * network-level "Account" page.
19743 *
19744 * @author Leo Fajardo (@leorw)
19745 */
19746 $this->switch_to_blog( $blog_id );
19747 }
19748
19749 $api = $this->get_api_site_scope();
19750
19751 if ( is_numeric( $site_license_id ) ) {
19752 // Try to retrieve a foreign license that is linked to the install.
19753 $api_result = $api->call( '/licenses.json?is_enriched=true' );
19754
19755 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
19756 is_array( $api_result->licenses )
19757 ) {
19758 $licenses = $api_result->licenses;
19759
19760 if ( ! empty( $licenses ) ) {
19761 $result[] = new FS_Plugin_License( $licenses[0] );
19762 }
19763 } else {
19764 $api_errors[] = $api_result;
19765 }
19766 } else if (
19767 is_object( $this->_license ) &&
19768 /**
19769 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
19770 * the FS instance that does the syncing is the parent FS instance.
19771 *
19772 * @author Leo Fajardo (@leorw)
19773 * @since 2.3.0
19774 */
19775 $this->_license->plugin_id == $plugin_id
19776 ) {
19777 $is_license_in_result = false;
19778 if ( ! empty( $result ) ) {
19779 foreach ( $result as $license ) {
19780 if ( $license->id == $this->_license->id ) {
19781 $is_license_in_result = true;
19782 break;
19783 }
19784 }
19785 }
19786
19787 if ( ! $is_license_in_result ) {
19788 // Fetch foreign license by ID and license key.
19789 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
19790 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
19791
19792 if ( $this->is_api_result_entity( $license ) ) {
19793 $result[] = new FS_Plugin_License( $license );
19794 } else {
19795 $api_errors[] = $license;
19796 }
19797 }
19798 }
19799
19800 if ( ! is_null( $blog_id ) ) {
19801 $this->switch_to_blog( $this->_storage->network_install_blog_id );
19802 }
19803 }
19804
19805 if ( is_array( $result ) && 0 < count( $result ) ) {
19806 // If found at least one license, return license collection even if there are errors.
19807 return $result;
19808 }
19809
19810 if ( ! empty( $api_errors ) ) {
19811 // If found any errors and no licenses, return first error.
19812 return $api_errors[0];
19813 }
19814
19815 // Fallback to empty licenses list.
19816 return $result;
19817 }
19818
19819 /**
19820 * @author Vova Feldman (@svovaf)
19821 * @since 2.0.0
19822 *
19823 * @param number $license_id
19824 * @param string $license_key
19825 *
19826 * @return \FS_Plugin_License|object
19827 */
19828 private function fetch_license_by_key( $license_id, $license_key ) {
19829 $this->_logger->entrance();
19830
19831 $api = $this->get_current_or_network_user_api_scope();
19832
19833 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
19834
19835 return $this->is_api_result_entity( $result ) ?
19836 new FS_Plugin_License( $result ) :
19837 $result;
19838 }
19839
19840 /**
19841 * @author Vova Feldman (@svovaf)
19842 * @since 1.2.0
19843 * @uses FS_Api
19844 *
19845 * @param number|bool $plugin_id
19846 * @param bool $flush
19847 *
19848 * @return FS_Payment[]|object
19849 */
19850 function _fetch_payments( $plugin_id = false, $flush = false ) {
19851 $this->_logger->entrance();
19852
19853 $api = $this->get_api_user_scope();
19854
19855 if ( ! is_numeric( $plugin_id ) ) {
19856 $plugin_id = $this->_plugin->id;
19857 }
19858
19859 $include_bundles = (
19860 is_object( $this->_plugin ) &&
19861 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
19862 );
19863
19864 $result = $api->get(
19865 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
19866 $flush
19867 );
19868
19869 if ( ! isset( $result->error ) ) {
19870 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
19871 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
19872 }
19873 $result = $result->payments;
19874 }
19875
19876 return $result;
19877 }
19878
19879 /**
19880 * @author Vova Feldman (@svovaf)
19881 * @since 1.2.1.5
19882 * @uses FS_Api
19883 *
19884 * @param bool $flush
19885 *
19886 * @return \FS_Billing|mixed
19887 */
19888 function _fetch_billing( $flush = false ) {
19889 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
19890
19891 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
19892
19893 if ( $this->is_api_result_entity( $billing ) ) {
19894 $billing = new FS_Billing( $billing );
19895 }
19896
19897 return $billing;
19898 }
19899
19900 /**
19901 * @author Vova Feldman (@svovaf)
19902 * @since 1.0.5
19903 *
19904 * @param FS_Plugin_License[] $licenses
19905 * @param number $module_id
19906 */
19907 private function _update_licenses( $licenses, $module_id ) {
19908 $this->_logger->entrance();
19909
19910 if ( is_array( $licenses ) ) {
19911 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
19912 $licenses[ $i ]->updated = time();
19913 }
19914 }
19915
19916 $this->_store_licenses( true, $module_id, $licenses );
19917 }
19918
19919 /**
19920 * @author Vova Feldman (@svovaf)
19921 * @since 1.0.4
19922 *
19923 * @param bool|number $plugin_id
19924 * @param bool $flush Since 1.1.7.3
19925 * @param int $expiration Since 1.2.2.7
19926 * @param bool|string $newer_than Since 2.2.1
19927 *
19928 * @return object|false New plugin tag info if exist.
19929 */
19930 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
19931 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
19932
19933 if ( ! is_object( $latest_tag ) ) {
19934 return false;
19935 }
19936
19937 $plugin_version = $this->get_plugin_version();
19938
19939 // Check if version is actually newer.
19940 $has_new_version =
19941 // If it's an non-installed add-on then always return latest.
19942 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
19943 // Compare versions.
19944 version_compare( $plugin_version, $latest_tag->version, '<' );
19945
19946 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
19947
19948 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
19949
19950 $this->_storage->beta_data = array(
19951 'is_beta' => $is_latest_version_beta,
19952 'version' => $latest_tag->version
19953 );
19954
19955 return $has_new_version ? $latest_tag : false;
19956 }
19957
19958 /**
19959 * @author Vova Feldman (@svovaf)
19960 * @since 1.0.5
19961 *
19962 * @param bool|number $plugin_id
19963 * @param bool $flush Since 1.1.7.3
19964 * @param int $expiration Since 1.2.2.7
19965 * @param bool|string $newer_than Since 2.2.1
19966 *
19967 * @return bool|FS_Plugin_Tag
19968 */
19969 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
19970 $this->_logger->entrance();
19971
19972 if ( ! is_numeric( $plugin_id ) ) {
19973 $plugin_id = $this->_plugin->id;
19974 }
19975
19976 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
19977 $updates = $this->get_all_updates();
19978
19979 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
19980 }
19981
19982 /**
19983 * Check if site assigned with active license.
19984 *
19985 * @author Vova Feldman (@svovaf)
19986 * @since 1.0.6
19987 *
19988 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
19989 */
19990 function has_active_license() {
19991 return (
19992 is_object( $this->_license ) &&
19993 is_numeric( $this->_license->id ) &&
19994 ! $this->_license->is_expired()
19995 );
19996 }
19997
19998 /**
19999 * Check if site assigned with active & valid (not expired) license.
20000 *
20001 * @author Vova Feldman (@svovaf)
20002 * @since 1.2.1
20003 *
20004 * @param bool $check_expiration
20005 */
20006 function has_active_valid_license( $check_expiration = true ) {
20007 return self::is_active_valid_license( $this->_license, $check_expiration );
20008 }
20009
20010 /**
20011 * @author Leo Fajardo (@leorw)
20012 * @since 2.3.1
20013 */
20014 function is_data_debug_mode() {
20015 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20016 return false;
20017 }
20018
20019 $fs = $this->is_addon() ?
20020 $this->get_parent_instance() :
20021 $this;
20022
20023 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20024 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20025 } else {
20026 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20027 }
20028
20029 return ( 'true' === $is_developer_license_debug_mode );
20030 }
20031
20032 /**
20033 * @author Leo Fajardo (@leorw)
20034 * @since 2.3.1
20035 */
20036 function _set_data_debug_mode() {
20037 if ( ! $this->is_whitelabeled( true ) ) {
20038 return;
20039 }
20040
20041 $license_or_user_key = fs_request_get( 'license_or_user_key' );
20042
20043 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20044 'true' :
20045 'false';
20046
20047 if ( 'true' === $transient_value ) {
20048 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20049 'last_license_key' :
20050 'last_license_user_key'
20051 );
20052
20053 if ( md5( $license_or_user_key ) !== $stored_key ) {
20054 $this->shoot_ajax_failure( sprintf(
20055 '%s... %s',
20056 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20057 $this->get_text_inline(
20058 'seems like the key you entered doesn\'t match our records.',
20059 'developer-or-license-not-found'
20060 )
20061 ) );
20062 }
20063 }
20064
20065 if ( $this->is_network_active() && fs_is_network_admin() ) {
20066 set_site_transient(
20067 "fs_{$this->get_id()}_data_debug_mode",
20068 $transient_value,
20069 WP_FS__TIME_24_HOURS_IN_SEC / 24
20070 );
20071 } else {
20072 set_transient(
20073 "fs_{$this->get_id()}_data_debug_mode",
20074 $transient_value,
20075 WP_FS__TIME_24_HOURS_IN_SEC / 24
20076 );
20077 }
20078
20079 if ( 'true' === $transient_value ) {
20080 $this->_admin_notices->add_sticky(
20081 $this->get_text_inline(
20082 '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.',
20083 'data_debug_mode_enabled'
20084 ),
20085 'data_debug_mode_enabled'
20086 );
20087 }
20088
20089 $this->shoot_ajax_success();
20090 }
20091
20092 /**
20093 * Check if a given license is active & valid (not expired).
20094 *
20095 * @author Vova Feldman (@svovaf)
20096 * @since 2.1.3
20097 *
20098 * @param FS_Plugin_License $license
20099 * @param bool $check_expiration
20100 *
20101 * @return bool
20102 */
20103 private static function is_active_valid_license( $license, $check_expiration = true ) {
20104 return (
20105 is_object( $license ) &&
20106 FS_Plugin_License::is_valid_id( $license->id ) &&
20107 $license->is_active() &&
20108 ( ! $check_expiration || $license->is_valid() )
20109 );
20110 }
20111
20112 /**
20113 * Checks if there's any site that is associated with an active & valid license.
20114 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20115 *
20116 * @author Vova Feldman (@svovaf)
20117 * @since 2.1.3
20118 *
20119 * @return bool
20120 */
20121 function has_any_active_valid_license() {
20122 if ( ! fs_is_network_admin() ) {
20123 return $this->has_active_valid_license();
20124 }
20125
20126 $installs = $this->get_blog_install_map();
20127 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20128
20129 foreach ( $installs as $blog_id => $install ) {
20130 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20131 continue;
20132 }
20133
20134 foreach ( $all_plugin_licenses as $license ) {
20135 if ( $license->id == $install->license_id ) {
20136 if ( self::is_active_valid_license( $license ) ) {
20137 return true;
20138 }
20139 }
20140 }
20141 }
20142
20143 return false;
20144 }
20145
20146 /**
20147 * Check if site assigned with license with enabled features.
20148 *
20149 * @author Vova Feldman (@svovaf)
20150 * @since 1.0.6
20151 *
20152 * @return bool
20153 */
20154 function has_features_enabled_license() {
20155 return (
20156 is_object( $this->_license ) &&
20157 is_numeric( $this->_license->id ) &&
20158 $this->_license->is_features_enabled()
20159 );
20160 }
20161
20162 /**
20163 * Checks if the product is activated with a bundle license.
20164 *
20165 * @author Leo Fajardo (@leorw)
20166 * @since 2.4.0
20167 *
20168 * @return bool
20169 */
20170 function is_activated_with_bundle_license() {
20171 if ( ! $this->has_features_enabled_license() ) {
20172 return false;
20173 }
20174
20175 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20176 }
20177
20178 /**
20179 * Check if user is a trial or have feature enabled license.
20180 *
20181 * @author Vova Feldman (@svovaf)
20182 * @since 1.1.7
20183 *
20184 * @return bool
20185 */
20186 function can_use_premium_code() {
20187 return $this->is_trial() || $this->has_features_enabled_license();
20188 }
20189
20190 /**
20191 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20192 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20193 * the context user is authenticated.
20194 *
20195 * @author Leo Fajardo (@leorw)
20196 * @since 1.2.2
20197 *
20198 * @return bool
20199 */
20200 function is_user_admin() {
20201 /**
20202 * Require a super-admin when network activated, running from the network level OR if
20203 * running from the site level but not delegated the opt-in.
20204 *
20205 * @author Vova Feldman (@svovaf)
20206 * @since 2.0.0
20207 */
20208 if ( $this->_is_network_active &&
20209 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20210 ) {
20211 return is_super_admin();
20212 }
20213
20214 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20215 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20216 }
20217
20218 /**
20219 * Sync site's plan.
20220 *
20221 * @author Vova Feldman (@svovaf)
20222 * @since 1.0.3
20223 *
20224 * @uses FS_Api
20225 *
20226 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20227 * the admin.
20228 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20229 * network-level "Account" page.
20230 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20231 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20232 * updates for a single site in case `execute_cron` has switched to a different blog.
20233 */
20234 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20235 $this->_logger->entrance();
20236
20237 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20238
20239 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20240
20241 if ( $is_addon_sync ) {
20242 $this->_sync_addon_license( $plugin_id, $background );
20243 } else {
20244 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20245 }
20246
20247 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20248 }
20249
20250 /**
20251 * Sync plugin's add-on license.
20252 *
20253 * @author Vova Feldman (@svovaf)
20254 * @since 1.0.6
20255 * @uses FS_Api
20256 *
20257 * @param number $addon_id
20258 * @param bool $background
20259 */
20260 private function _sync_addon_license( $addon_id, $background ) {
20261 $this->_logger->entrance();
20262
20263 if ( $this->is_addon_activated( $addon_id ) ) {
20264 // If already installed, use add-on sync.
20265 $fs_addon = self::get_instance_by_id( $addon_id );
20266
20267 if (
20268 // Add-on is network activated and network integrated.
20269 $fs_addon->is_network_active() ||
20270 // Background sync cron.
20271 self::is_cron() ||
20272 // Add-on is not network activated or not network integrated.
20273 ! fs_is_network_admin()
20274 ) {
20275 $fs_addon->_sync_license( $background );
20276
20277 return;
20278 }
20279 }
20280
20281 // Validate add-on exists.
20282 $addon = $this->get_addon( $addon_id );
20283
20284 if ( ! is_object( $addon ) ) {
20285 return;
20286 }
20287
20288 // Add add-on into account add-ons.
20289 $account_addons = $this->get_account_addons();
20290 if ( ! is_array( $account_addons ) ) {
20291 $account_addons = array();
20292 }
20293 $account_addons[] = $addon->id;
20294 $account_addons = array_unique( $account_addons );
20295 $this->_store_account_addons( $account_addons );
20296
20297 // Load add-on licenses.
20298 $licenses = $this->_fetch_licenses( $addon->id );
20299
20300 // Sync add-on licenses.
20301 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
20302 $this->_update_licenses( $licenses, $addon->id );
20303
20304 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
20305 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
20306
20307 if ( ! isset( $plans_result->error ) ) {
20308 $plans = array();
20309 foreach ( $plans_result->plans as $plan ) {
20310 $plans[] = new FS_Plugin_Plan( $plan );
20311 }
20312
20313 $this->_admin_notices->add_sticky(
20314 sprintf(
20315 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
20316 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
20317 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
20318 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
20319 $addon->title
20320 ) . ' ' . $this->get_latest_download_link(
20321 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
20322 $addon_id
20323 ),
20324 'addon_plan_upgraded_' . $addon->slug,
20325 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20326 );
20327 }
20328 }
20329 }
20330 }
20331
20332 /**
20333 * Sync site's plugin plan.
20334 *
20335 * @author Vova Feldman (@svovaf)
20336 * @since 1.0.6
20337 * @uses FS_Api
20338 *
20339 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
20340 * @param bool $send_installs_update Since 2.0.0
20341 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
20342 * syncing its license from the network-level "Account" page (e.g.: after
20343 * activating a license only for the single install).
20344 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
20345 * can be used here to switch to the previous blog in case `execute_cron`
20346 * has switched to a different blog.
20347 */
20348 private function _sync_plugin_license(
20349 $background = false,
20350 $send_installs_update = true,
20351 $is_context_single_site = false,
20352 $current_blog_id = null
20353 ) {
20354 $this->_logger->entrance();
20355
20356 $plan_change = 'none';
20357
20358 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
20359
20360 if ( ! $send_installs_update ) {
20361 $site = $this->_site;
20362 } else {
20363 /**
20364 * Sync site info.
20365 *
20366 * @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.
20367 */
20368 if ( $is_site_level_sync ) {
20369 /**
20370 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
20371 *
20372 * @author Leo Fajardo (@leorw)
20373 * @since 2.2.3
20374 */
20375 if ( is_numeric( $current_blog_id ) ) {
20376 $this->switch_to_blog( $current_blog_id );
20377 }
20378
20379 $result = $this->send_install_update( array(), true );
20380 $is_valid = $this->is_api_result_entity( $result );
20381 } else {
20382 $result = $this->send_installs_update( array(), true );
20383 $is_valid = $this->is_api_result_object( $result, 'installs' );
20384 }
20385
20386 if ( ! $is_valid ) {
20387 if ( $is_context_single_site ) {
20388 // Switch back to the main blog so that the following logic will have the right entities.
20389 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20390 }
20391
20392 // Show API messages only if not background sync or if paying customer.
20393 if ( ! $background || $this->is_paying() ) {
20394 // Try to ping API to see if not blocked.
20395 if ( ! FS_Api::test() ) {
20396 /**
20397 * Failed to ping API - blocked!
20398 *
20399 * @author Vova Feldman (@svovaf)
20400 * @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.
20401 */
20402 $api = $this->get_api_site_scope();
20403
20404 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
20405 self::$_global_admin_notices->add(
20406 sprintf(
20407 $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 %2$s', 'server-blocking-access' ),
20408 $this->get_plugin_name(),
20409 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
20410 'api.freemius.com',
20411 'wp.freemius.com'
20412 ) ) ) . '</b>'
20413 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
20414 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
20415 'error',
20416 $background,
20417 'api_blocked'
20418 );
20419 }
20420 } else {
20421 // Authentication params are broken.
20422 $this->_admin_notices->add(
20423 $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 ),
20424 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
20425 'error'
20426 );
20427 }
20428 }
20429
20430 // No reason to continue with license sync while there are API issues.
20431 return;
20432 }
20433
20434 if ( $is_site_level_sync ) {
20435 $site = new FS_Site( $result );
20436 } else {
20437 // Map site addresses to their blog IDs.
20438 $address_to_blog_map = $this->get_address_to_blog_map();
20439
20440 // Find the current context install.
20441 $site = null;
20442 foreach ( $result->installs as $install ) {
20443 if ( $install->id == $this->_site->id ) {
20444 $site = new FS_Site( $install );
20445 } else {
20446 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
20447 $blog_id = $address_to_blog_map[ $address ];
20448
20449 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
20450 }
20451 }
20452 }
20453
20454 // Sync plans.
20455 $this->_sync_plans();
20456 }
20457
20458 // Remove sticky API connectivity message.
20459 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
20460
20461 if ( ! $this->has_paid_plan() ) {
20462 $this->_site = $site;
20463 $this->_store_site(
20464 true,
20465 $is_site_level_sync ?
20466 null :
20467 $this->get_network_install_blog_id()
20468 );
20469 } else {
20470 $context_blog_id = 0;
20471
20472 if ( $is_context_single_site ) {
20473 $context_blog_id = get_current_blog_id();
20474
20475 // Switch back to the main blog in order to properly sync the license.
20476 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20477 }
20478
20479 /**
20480 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
20481 * associated with that ID is not included in the user's licenses collection.
20482 */
20483 $this->_sync_licenses(
20484 $site->license_id,
20485 ( $is_context_single_site ?
20486 $context_blog_id :
20487 null
20488 )
20489 );
20490
20491 if ( $is_context_single_site ) {
20492 $this->switch_to_blog( $context_blog_id );
20493 }
20494
20495 // Check if plan / license changed.
20496 if ( $site->plan_id != $this->_site->plan_id ||
20497 // Check if trial started.
20498 $site->trial_plan_id != $this->_site->trial_plan_id ||
20499 $site->trial_ends != $this->_site->trial_ends ||
20500 // Check if license changed.
20501 $site->license_id != $this->_site->license_id
20502 ) {
20503 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
20504 // New trial started.
20505 $this->_site = $site;
20506 $plan_change = 'trial_started';
20507
20508 // For trial with subscription use-case.
20509 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
20510
20511 if ( is_object( $new_license ) && $new_license->is_valid() ) {
20512 $this->_site = $site;
20513 $this->_update_site_license( $new_license );
20514 $this->_store_licenses();
20515
20516 $this->_sync_site_subscription( $this->_license );
20517 }
20518 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
20519 // Was in trial, but now trial expired and no license ID.
20520 // New trial started.
20521 $this->_site = $site;
20522 $plan_change = 'trial_expired';
20523 } else {
20524 $is_free = $this->is_free_plan();
20525
20526 // Make sure license exist and not expired.
20527 $new_license = is_null( $site->license_id ) ?
20528 null :
20529 $this->_get_license_by_id( $site->license_id );
20530
20531 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
20532 // License cancelled.
20533 $this->_site = $site;
20534 $this->_update_site_license( $new_license );
20535 $this->_store_licenses();
20536
20537 $plan_change = 'cancelled';
20538 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
20539 // The license is expired, so ignore upgrade method.
20540 $this->_site = $site;
20541 } else {
20542 // License changed.
20543 $this->_site = $site;
20544
20545 /**
20546 * IMPORTANT:
20547 * 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.
20548 *
20549 * @author Vova Feldman (@svovaf)
20550 * @since 2.0.0
20551 */
20552 $this->_update_site_license( $new_license );
20553
20554 if ( ! $is_context_single_site &&
20555 fs_is_network_admin() &&
20556 $this->_is_network_active &&
20557 $new_license->quota > 1 &&
20558 get_blog_count() > 1
20559 ) {
20560 // See if license can activated on all sites.
20561 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
20562 if ( ! fs_request_get_bool( 'auto_install' ) ) {
20563 // Open the license activation dialog box on the account page.
20564 add_action( 'admin_footer', array(
20565 &$this,
20566 '_open_license_activation_dialog_box'
20567 ) );
20568 }
20569 }
20570 }
20571
20572 $this->_store_licenses();
20573
20574 $plan_change = $is_free ?
20575 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
20576 ( is_object( $new_license ) ?
20577 'changed' :
20578 'downgraded' );
20579 }
20580 }
20581
20582 // Store updated site info.
20583 $this->_store_site(
20584 true,
20585 $is_site_level_sync ?
20586 null :
20587 $this->get_network_install_blog_id()
20588 );
20589 } else {
20590 if ( ! is_object( $this->_license ) ) {
20591 $this->maybe_update_whitelabel_flag(
20592 FS_Plugin_License::is_valid_id( $site->license_id ) ?
20593 $this->get_license_by_id( $site->license_id ) :
20594 null
20595 );
20596 } else {
20597 $this->maybe_update_whitelabel_flag( $this->_license );
20598
20599 if ( $this->_license->is_expired() ) {
20600 if ( ! $this->has_features_enabled_license() ) {
20601 $this->_deactivate_license();
20602 $plan_change = 'downgraded';
20603 } else {
20604 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
20605
20606 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
20607 /**
20608 * Show the expired license notice every 14 days.
20609 *
20610 * @author Leo Fajardo (@leorw)
20611 * @since 2.3.1
20612 */
20613 $plan_change = 'expired';
20614 }
20615 }
20616 }
20617 }
20618
20619 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
20620 $this->_sync_site_subscription( $this->_license );
20621 }
20622 }
20623
20624 if ( ! $this->is_addon() &&
20625 $this->_site->is_beta() !== $site->is_beta
20626 ) {
20627 // Beta flag updated.
20628 $this->_site = $site;
20629
20630 $this->_store_site(
20631 true,
20632 $is_site_level_sync ?
20633 null :
20634 $this->get_network_install_blog_id()
20635 );
20636 }
20637
20638 if ( $this->is_addon() || $this->has_addons() ) {
20639 /**
20640 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
20641 * an updated valid user licenses collection will be fetched from the server which is used to also
20642 * update the account add-ons (add-ons the user has licenses for).
20643 *
20644 * @author Leo Fajardo (@leorw)
20645 * @since 2.2.4
20646 */
20647 $this->purge_valid_user_licenses_cache();
20648 }
20649 }
20650
20651 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
20652
20653 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
20654 switch ( $plan_change ) {
20655 case 'none':
20656 if ( ! $background && is_admin() ) {
20657 $plan = $this->is_trial() ?
20658 $this->get_trial_plan() :
20659 $this->get_plan();
20660
20661 if ( $plan->is_free() ) {
20662 $this->_admin_notices->add(
20663 sprintf(
20664 $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' ),
20665 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
20666 ) . ' ' . sprintf(
20667 '<a href="%s">%s</a>',
20668 $this->contact_url(
20669 'bug',
20670 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' ),
20671 strtoupper( $plan->name )
20672 )
20673 ),
20674 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
20675 ),
20676 $hmm_text
20677 );
20678 }
20679 }
20680 break;
20681 case 'upgraded':
20682 case 'activated':
20683 $this->_admin_notices->add_sticky(
20684 ( 'activated' === $plan_change ) ?
20685 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ) :
20686 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) .
20687 $this->get_complete_upgrade_instructions(),
20688 'plan_upgraded',
20689 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20690 );
20691
20692 $this->_admin_notices->remove_sticky( array(
20693 'trial_started',
20694 'trial_promotion',
20695 'trial_expired',
20696 'activation_complete',
20697 'license_expired',
20698 ) );
20699 break;
20700 case 'changed':
20701 $this->_admin_notices->add_sticky(
20702 sprintf(
20703 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
20704 $this->get_plan_title()
20705 ),
20706 'plan_changed'
20707 );
20708
20709 $this->_admin_notices->remove_sticky( array(
20710 'trial_started',
20711 'trial_promotion',
20712 'trial_expired',
20713 'activation_complete',
20714 ) );
20715 break;
20716 case 'downgraded':
20717 $this->_admin_notices->add_sticky(
20718 ($this->has_free_plan() ?
20719 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 ) :
20720 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
20721 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) ) ),
20722 'license_expired',
20723 $hmm_text
20724 );
20725 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20726 break;
20727 case 'cancelled':
20728 $this->_admin_notices->add(
20729 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
20730 sprintf(
20731 '<a href="%s">%s</a>',
20732 $this->contact_url( 'bug' ),
20733 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
20734 ),
20735 $hmm_text,
20736 'error'
20737 );
20738 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20739 break;
20740 case 'expired':
20741 $this->_admin_notices->add_sticky(
20742 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 ),
20743 'license_expired',
20744 $hmm_text
20745 );
20746
20747 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
20748
20749 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20750 break;
20751 case 'trial_started':
20752 $this->_admin_notices->add_sticky(
20753 sprintf(
20754 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
20755 '<i>' . $this->get_plugin_name() . '</i>'
20756 ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ),
20757 'trial_started',
20758 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20759 );
20760
20761 $this->_admin_notices->remove_sticky( array(
20762 'trial_promotion',
20763 ) );
20764 break;
20765 case 'trial_expired':
20766 $this->_admin_notices->add_sticky(
20767 ($this->has_free_plan() ?
20768 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
20769 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
20770 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))),
20771 'trial_expired',
20772 $hmm_text
20773 );
20774 $this->_admin_notices->remove_sticky( array(
20775 'trial_started',
20776 'trial_promotion',
20777 'plan_upgraded',
20778 ) );
20779 break;
20780 }
20781 }
20782
20783 if ( 'none' !== $plan_change ) {
20784 if (
20785 ! is_object( $this->_license ) ||
20786 ! $this->_license->is_whitelabeled
20787 ) {
20788 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
20789 }
20790
20791 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
20792 }
20793 }
20794
20795 /**
20796 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
20797 *
20798 * @author Vova Feldman (@svovaf)
20799 * @since 2.0.0
20800 */
20801 public function _open_license_activation_dialog_box() {
20802 $vars = array( 'license_id' => $this->_site->license_id );
20803 fs_require_once_template( 'js/open-license-activation.php', $vars );
20804 }
20805
20806 /**
20807 * @author Vova Feldman (@svovaf)
20808 * @since 1.0.5
20809 *
20810 * @param bool $background
20811 * @param FS_Plugin_License|null $premium_license
20812 */
20813 protected function _activate_license( $background = false, $premium_license = null ) {
20814 $this->_logger->entrance();
20815
20816 if ( is_null( $premium_license ) ) {
20817 $license_id = fs_request_get( 'license_id' );
20818
20819 if ( is_object( $this->_site ) &&
20820 FS_Plugin_License::is_valid_id( $license_id ) &&
20821 $license_id == $this->_site->license_id
20822 ) {
20823 // License is already activated.
20824 return;
20825 }
20826
20827 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
20828 $this->_get_license_by_id( $license_id ) :
20829 $this->_get_available_premium_license();
20830 }
20831
20832 if ( ! is_object( $premium_license ) ) {
20833 return;
20834 }
20835
20836 if ( ! is_object( $this->_site ) ) {
20837 // Not yet opted-in.
20838 $user = $this->get_current_or_network_user();
20839 if ( ! is_object( $user ) ) {
20840 $user = self::_get_user_by_id( $premium_license->user_id );
20841 }
20842
20843 if ( is_object( $user ) ) {
20844 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
20845 } else {
20846 $this->opt_in(
20847 false,
20848 false,
20849 false,
20850 $premium_license->secret_key
20851 );
20852
20853 return;
20854 }
20855 }
20856
20857
20858 /**
20859 * If the premium license is already associated with the install, just
20860 * update the license reference (activation is not required).
20861 *
20862 * @since 1.1.9
20863 */
20864 if ( $premium_license->id == $this->_site->license_id ) {
20865 // License is already activated.
20866 $this->_update_site_license( $premium_license );
20867 $this->_store_account();
20868
20869 return;
20870 }
20871
20872 if ( $this->_site->user_id != $premium_license->user_id ) {
20873 $api_request_params = array( 'license_key' => $premium_license->secret_key );
20874 } else {
20875 $api_request_params = array();
20876 }
20877
20878 $api = $this->get_api_site_scope();
20879 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
20880
20881 if ( ! $this->is_api_result_entity( $license ) ) {
20882 if ( ! $background ) {
20883 $this->_admin_notices->add( sprintf(
20884 '%s %s',
20885 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
20886 ( is_object( $license ) && isset( $license->error ) ?
20887 $license->error->message :
20888 sprintf( '%s<br><code>%s</code>',
20889 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
20890 var_export( $license, true )
20891 )
20892 )
20893 ),
20894 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
20895 'error'
20896 );
20897 }
20898
20899 return;
20900 }
20901
20902 $premium_license = new FS_Plugin_License( $license );
20903
20904 // Updated site plan.
20905 $site = $this->get_api_site_scope()->get( '/', true );
20906 if ( $this->is_api_result_entity( $site ) ) {
20907 $this->_site = new FS_Site( $site );
20908 }
20909 $this->_update_site_license( $premium_license );
20910
20911 $this->_store_account();
20912
20913 if ( $this->is_addon() || $this->has_addons() ) {
20914 /**
20915 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
20916 * an updated valid user licenses collection will be fetched from the server which is used to also
20917 * update the account add-ons (add-ons the user has licenses for).
20918 *
20919 * @author Leo Fajardo (@leorw)
20920 * @since 2.2.4
20921 */
20922 $this->purge_valid_user_licenses_cache();
20923 }
20924
20925 if ( ! $background ) {
20926 $this->_admin_notices->add_sticky(
20927 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
20928 $this->get_complete_upgrade_instructions(),
20929 'license_activated',
20930 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20931 );
20932 }
20933
20934 $this->_admin_notices->remove_sticky( array(
20935 'trial_promotion',
20936 'license_expired',
20937 ) );
20938 }
20939
20940 /**
20941 * @author Vova Feldman (@svovaf)
20942 * @since 1.0.5
20943 *
20944 * @param bool $show_notice
20945 */
20946 protected function _deactivate_license( $show_notice = true ) {
20947 $this->_logger->entrance();
20948
20949 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
20950
20951 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
20952 $this->_admin_notices->add(
20953 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() ),
20954 $hmm_text
20955 );
20956
20957 return;
20958 }
20959
20960 $api = $this->get_api_site_scope();
20961 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
20962
20963 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
20964 }
20965
20966 /**
20967 * @author Leo Fajardo (@leorw)
20968 * @since 2.2.1
20969 *
20970 * @param FS_Plugin_License $license
20971 * @param bool|string $hmm_text
20972 * @param bool $show_notice
20973 */
20974 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
20975 if ( isset( $license->error ) ) {
20976 $this->_admin_notices->add(
20977 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
20978 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
20979 $hmm_text,
20980 'error'
20981 );
20982
20983 return;
20984 }
20985
20986 // Update license cache.
20987 if ( is_array( $this->_licenses ) ) {
20988 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
20989 if ( $license->id == $this->_licenses[ $i ]->id ) {
20990 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
20991 }
20992 }
20993 }
20994
20995 // Update site plan to default.
20996 $this->_sync_plans();
20997 $this->_site->plan_id = $this->_plans[0]->id;
20998 // Unlink license from site.
20999 $this->_update_site_license( null );
21000
21001 $this->_store_account();
21002
21003 if ( $show_notice ) {
21004 $this->_admin_notices->add(
21005 sprintf( $this->is_only_premium() ?
21006 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21007 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21008 $this->get_plan_title()
21009 ),
21010 $this->get_text_inline( 'O.K', 'ok' )
21011 );
21012 }
21013
21014 $this->_admin_notices->remove_sticky( array(
21015 'plan_upgraded',
21016 'license_activated',
21017 ) );
21018 }
21019
21020 /**
21021 * Site plan downgrade.
21022 *
21023 * @author Vova Feldman (@svovaf)
21024 * @since 1.0.4
21025 *
21026 * @return object
21027 *
21028 * @uses FS_Api
21029 */
21030 private function _downgrade_site() {
21031 $this->_logger->entrance();
21032
21033 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21034
21035 $api = $this->get_api_site_scope();
21036 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21037
21038 $plan_downgraded = false;
21039 $plan = false;
21040 if ( $this->is_api_result_entity( $site ) ) {
21041 $prev_plan_id = $this->_site->plan_id;
21042
21043 // Update new site plan id.
21044 $this->_site->plan_id = $site->plan_id;
21045
21046 $plan = $this->get_plan();
21047 $subscription = $this->_sync_site_subscription( $this->_license );
21048
21049 // Plan downgraded if plan was changed or subscription was cancelled.
21050 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21051 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21052 } else {
21053 // handle different error cases.
21054 $this->handle_license_deactivation_result(
21055 $site,
21056 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21057 );
21058 }
21059
21060 if ( ! $plan_downgraded ) {
21061 return (object) array(
21062 'error' => (object) array(
21063 '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' )
21064 )
21065 );
21066 }
21067
21068 // Remove previous sticky message about upgrade (if exist).
21069 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21070
21071 $this->_admin_notices->add(
21072 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21073 $plan->title,
21074 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21075 )
21076 );
21077
21078 // Store site updates.
21079 $this->_store_site();
21080
21081 if ( $deactivate_license &&
21082 ! FS_Plugin_License::is_valid_id( $site->license_id )
21083 ) {
21084 if ( $this->_site->is_localhost() ) {
21085 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21086 } else {
21087 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21088 }
21089
21090 // Handle successful license deactivation result.
21091 $this->handle_license_deactivation_result( $this->_license );
21092 }
21093
21094 return $site;
21095 }
21096
21097 /**
21098 * @author Vova Feldman (@svovaf)
21099 * @since 1.1.8.1
21100 *
21101 * @param bool|string $plan_name
21102 *
21103 * @return bool If trial was successfully started.
21104 */
21105 function start_trial( $plan_name = false ) {
21106 $this->_logger->entrance();
21107
21108 // Alias.
21109 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21110
21111 if ( $this->is_trial() ) {
21112 // Already in trial mode.
21113 $this->_admin_notices->add(
21114 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21115 $oops_text,
21116 'error'
21117 );
21118
21119 return false;
21120 }
21121
21122 if ( $this->_site->is_trial_utilized() ) {
21123 // Trial was already utilized.
21124 $this->_admin_notices->add(
21125 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21126 $oops_text,
21127 'error'
21128 );
21129
21130 return false;
21131 }
21132
21133 if ( false !== $plan_name ) {
21134 $plan = $this->get_plan_by_name( $plan_name );
21135
21136 if ( false === $plan ) {
21137 // Plan doesn't exist.
21138 $this->_admin_notices->add(
21139 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21140 $oops_text,
21141 'error'
21142 );
21143
21144 return false;
21145 }
21146
21147 if ( ! $plan->has_trial() ) {
21148 // Plan doesn't exist.
21149 $this->_admin_notices->add(
21150 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21151 $oops_text,
21152 'error'
21153 );
21154
21155 return false;
21156 }
21157 } else {
21158 if ( ! $this->has_trial_plan() ) {
21159 // None of the plans have a trial.
21160 $this->_admin_notices->add(
21161 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21162 $oops_text,
21163 'error'
21164 );
21165
21166 return false;
21167 }
21168
21169 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21170
21171 $plan = $plans_with_trial[0];
21172 }
21173
21174 $api = $this->get_api_site_scope();
21175 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
21176
21177 if ( ! $this->is_api_result_entity( $plan ) ) {
21178 // Some API error while trying to start the trial.
21179 $this->_admin_notices->add(
21180 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type )
21181 . ' ' . var_export( $plan, true ),
21182 $oops_text,
21183 'error'
21184 );
21185
21186 return false;
21187 }
21188
21189 // Sync license.
21190 $this->_sync_license();
21191
21192 return $this->is_trial();
21193 }
21194
21195 /**
21196 * Cancel site trial.
21197 *
21198 * @author Vova Feldman (@svovaf)
21199 * @since 1.0.9
21200 *
21201 * @return object
21202 *
21203 * @uses FS_Api
21204 */
21205 private function _cancel_trial() {
21206 $this->_logger->entrance();
21207
21208 if ( ! $this->is_trial() ) {
21209 return (object) array(
21210 'error' => (object) array(
21211 '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' )
21212 )
21213 );
21214 }
21215
21216 $trial_plan = $this->get_trial_plan();
21217
21218 $api = $this->get_api_site_scope();
21219 $site = $api->call( 'trials.json', 'delete' );
21220
21221 $trial_cancelled = false;
21222
21223 if ( $this->is_api_result_entity( $site ) ) {
21224 $prev_trial_ends = $this->_site->trial_ends;
21225
21226 if ( $this->is_paid_trial() ) {
21227 $this->_license->expiration = $site->trial_ends;
21228 $this->_license->is_cancelled = true;
21229 $this->_update_site_license( $this->_license );
21230 $this->_store_licenses();
21231
21232 // Clear subscription reference.
21233 $this->_sync_site_subscription( null );
21234 }
21235
21236 // Update site info.
21237 $this->_site = new FS_Site( $site );
21238
21239 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
21240 } else {
21241 // @todo handle different error cases.
21242 }
21243
21244 if ( ! $trial_cancelled ) {
21245 return (object) array(
21246 'error' => (object) array(
21247 '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' )
21248 )
21249 );
21250 }
21251
21252 // Remove previous sticky messages about upgrade or trial (if exist).
21253 $this->_admin_notices->remove_sticky( array(
21254 'trial_started',
21255 'trial_promotion',
21256 'plan_upgraded',
21257 ) );
21258
21259 // Store site updates.
21260 $this->_store_site();
21261
21262 if ( ! $this->is_addon() ||
21263 ! $this->deactivate_premium_only_addon_without_license( true )
21264 ) {
21265 $this->_admin_notices->add(
21266 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
21267 );
21268 }
21269
21270 return $site;
21271 }
21272
21273 /**
21274 * @author Vova Feldman (@svovaf)
21275 * @since 1.0.6
21276 *
21277 * @param bool|number $plugin_id
21278 *
21279 * @return bool
21280 */
21281 private function _is_addon_id( $plugin_id ) {
21282 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
21283 }
21284
21285 /**
21286 * Check if user eligible to download premium version updates.
21287 *
21288 * @author Vova Feldman (@svovaf)
21289 * @since 1.0.6
21290 *
21291 * @return bool
21292 */
21293 private function _can_download_premium() {
21294 return $this->has_any_active_valid_license() ||
21295 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
21296 }
21297
21298 /**
21299 *
21300 * @author Vova Feldman (@svovaf)
21301 * @since 1.0.6
21302 *
21303 * @param bool|number $addon_id
21304 * @param string $type "json" or "zip"
21305 *
21306 * @return string
21307 */
21308 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
21309
21310 $is_addon = $this->_is_addon_id( $addon_id );
21311
21312 $is_premium = null;
21313 if ( ! $is_addon ) {
21314 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
21315 } else if ( $this->is_addon_activated( $addon_id ) ) {
21316 $fs_addon = self::get_instance_by_id( $addon_id );
21317 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
21318 }
21319
21320 // If add-on, then append add-on ID.
21321 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
21322 '/updates/latest.' . $type;
21323
21324 // If add-on and not yet activated, try to fetch based on server licensing.
21325 if ( is_bool( $is_premium ) ) {
21326 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
21327 }
21328
21329 if ( $this->has_secret_key() ) {
21330 $endpoint = add_query_arg( 'type', 'all', $endpoint );
21331 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
21332 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
21333 }
21334
21335 return $endpoint;
21336 }
21337
21338 /**
21339 * @author Vova Feldman (@svovaf)
21340 * @since 1.0.4
21341 *
21342 * @param bool|number $addon_id
21343 * @param bool $flush Since 1.1.7.3
21344 * @param int $expiration Since 1.2.2.7
21345 * @param bool|string $newer_than Since 2.2.1
21346 * @param bool|string $fetch_readme Since 2.2.1
21347 *
21348 * @return object|false Plugin latest tag info.
21349 */
21350 function _fetch_latest_version(
21351 $addon_id = false,
21352 $flush = true,
21353 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
21354 $newer_than = false,
21355 $fetch_readme = true
21356 ) {
21357 $this->_logger->entrance();
21358
21359 $switch_to_blog_id = null;
21360
21361 /**
21362 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
21363 * @since 1.1.7.4 Also check updates for add-ons.
21364 */
21365 if ( ! $this->is_registered() &&
21366 ! $this->_is_addon_id( $addon_id )
21367 ) {
21368 if ( ! is_multisite() ) {
21369 return false;
21370 }
21371
21372 $installs_map = $this->get_blog_install_map();
21373
21374 foreach ( $installs_map as $blog_id => $install ) {
21375 /**
21376 * @var FS_Site $install
21377 */
21378 if ( $install->is_trial() ) {
21379 $switch_to_blog_id = $blog_id;
21380 break;
21381 }
21382
21383 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
21384 $license = $this->get_license_by_id( $install->license_id );
21385
21386 if ( is_object( $license ) && $license->is_features_enabled() ) {
21387 $switch_to_blog_id = $blog_id;
21388 break;
21389 }
21390 }
21391 }
21392
21393 if ( is_null( $switch_to_blog_id ) ) {
21394 return false;
21395 }
21396 }
21397
21398 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
21399 get_current_blog_id() :
21400 0;
21401
21402 if ( is_numeric( $switch_to_blog_id ) ) {
21403 $this->switch_to_blog( $switch_to_blog_id );
21404 }
21405
21406 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
21407
21408 if ( ! empty( $newer_than ) ) {
21409 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
21410 }
21411
21412 if ( true === $fetch_readme ) {
21413 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
21414 }
21415
21416 $tag = $this->get_api_site_or_plugin_scope()->get(
21417 $latest_version_endpoint,
21418 $flush,
21419 $expiration
21420 );
21421
21422 if ( is_numeric( $switch_to_blog_id ) ) {
21423 $this->switch_to_blog( $current_blog_id );
21424 }
21425
21426 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
21427
21428 $this->_logger->departure( 'Latest version ' . $latest_version );
21429
21430 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
21431 }
21432
21433 #----------------------------------------------------------------------------------
21434 #region Download Plugin
21435 #----------------------------------------------------------------------------------
21436
21437 /**
21438 * Download latest plugin version, based on plan.
21439 *
21440 * Not like _download_latest(), this will redirect the page
21441 * to secure download url to prevent dual download (from FS to WP server,
21442 * and then from WP server to the client / browser).
21443 *
21444 * @author Vova Feldman (@svovaf)
21445 * @since 1.0.9
21446 *
21447 * @param bool|number $plugin_id
21448 *
21449 * @uses FS_Api
21450 * @uses wp_redirect()
21451 */
21452 private function download_latest_directly( $plugin_id = false ) {
21453 $this->_logger->entrance();
21454
21455 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
21456 }
21457
21458 /**
21459 * Get latest plugin FS API download URL.
21460 *
21461 * @author Vova Feldman (@svovaf)
21462 * @since 1.0.9
21463 *
21464 * @param bool|number $plugin_id
21465 *
21466 * @return string
21467 */
21468 private function get_latest_download_api_url( $plugin_id = false ) {
21469 $this->_logger->entrance();
21470
21471 return $this->get_api_site_scope()->get_signed_url(
21472 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
21473 );
21474 }
21475
21476 /**
21477 * Get payment invoice URL.
21478 *
21479 * @author Vova Feldman (@svovaf)
21480 * @since 1.2.0
21481 *
21482 * @param bool|number $payment_id
21483 *
21484 * @return string
21485 */
21486 function _get_invoice_api_url( $payment_id = false ) {
21487 $this->_logger->entrance();
21488
21489 $url = $this->get_api_user_scope()->get_signed_url(
21490 "/payments/{$payment_id}/invoice.pdf"
21491 );
21492
21493 if ( ! fs_starts_with( $url, 'https://' ) ) {
21494 // Always use HTTPS for invoices.
21495 $url = 'https' . substr( $url, 4 );
21496 }
21497
21498 return $url;
21499 }
21500
21501 /**
21502 * Get latest plugin download link.
21503 *
21504 * @author Vova Feldman (@svovaf)
21505 * @since 1.0.9
21506 *
21507 * @param string $label
21508 * @param bool|number $plugin_id
21509 *
21510 * @return string
21511 */
21512 private function get_latest_download_link( $label, $plugin_id = false ) {
21513 return sprintf(
21514 '<a target="_blank" rel="noopener" href="%s">%s</a>',
21515 $this->_get_latest_download_local_url( $plugin_id ),
21516 $label
21517 );
21518 }
21519
21520 /**
21521 * Get latest plugin download local URL.
21522 *
21523 * @author Vova Feldman (@svovaf)
21524 * @since 1.0.9
21525 *
21526 * @param bool|number $plugin_id
21527 *
21528 * @return string
21529 */
21530 function _get_latest_download_local_url( $plugin_id = false ) {
21531 // Add timestamp to protect from caching.
21532 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
21533
21534 if ( ! empty( $plugin_id ) ) {
21535 $params['plugin_id'] = $plugin_id;
21536 } else if ( $this->is_addon() ) {
21537 $params['plugin_id'] = $this->get_id();
21538 }
21539
21540 $fs = $this->is_addon() ?
21541 $this->get_parent_instance() :
21542 $this;
21543
21544 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
21545 }
21546
21547 #endregion Download Plugin ------------------------------------------------------------------
21548
21549 /**
21550 * @author Vova Feldman (@svovaf)
21551 * @since 1.0.4
21552 *
21553 * @uses FS_Api
21554 *
21555 * @param bool $background Hints the method if it's a background updates check. If false, it means that
21556 * was initiated by the admin.
21557 * @param bool|number $plugin_id
21558 * @param bool $flush Since 1.1.7.3
21559 * @param int $expiration Since 1.2.2.7
21560 * @param bool|string $newer_than Since 2.2.1
21561 */
21562 private function check_updates(
21563 $background = false,
21564 $plugin_id = false,
21565 $flush = true,
21566 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
21567 $newer_than = false
21568 ) {
21569 $this->_logger->entrance();
21570
21571 // Check if there's a newer version for download.
21572 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
21573
21574 $update = null;
21575 if ( is_object( $new_version ) ) {
21576 $update = new FS_Plugin_Tag( $new_version );
21577
21578 if ( ! $background ) {
21579 $this->_admin_notices->add(
21580 sprintf(
21581 /* translators: %s: Numeric version number (e.g. '2.1.9' */
21582 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
21583 $update->version,
21584 sprintf(
21585 '<a href="%s" target="_blank" rel="noopener">%s</a>',
21586 $this->get_account_url( 'download_latest' ),
21587 sprintf(
21588 /* translators: %s: plan name (e.g. latest "Professional" version) */
21589 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
21590 $this->get_plan_title()
21591 )
21592 )
21593 ),
21594 $this->get_text_inline( 'New', 'new' ) . '!'
21595 );
21596 }
21597 } else if ( false === $new_version && ! $background ) {
21598 $this->_admin_notices->add(
21599 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
21600 $this->get_text_inline( 'You are all good!', 'you-are-good' )
21601 );
21602 }
21603
21604 $this->_store_update( $update, true, $plugin_id );
21605 }
21606
21607 /**
21608 * @author Vova Feldman (@svovaf)
21609 * @since 1.0.4
21610 *
21611 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
21612 *
21613 * @return FS_Plugin[]
21614 *
21615 * @uses FS_Api
21616 */
21617 private function sync_addons( $flush = false ) {
21618 $this->_logger->entrance();
21619
21620 $api = $this->get_api_site_or_plugin_scope();
21621
21622 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
21623
21624 /**
21625 * @since 1.2.1
21626 *
21627 * If there's a cached version of the add-ons and not asking
21628 * for a flush, just use the currently stored add-ons.
21629 */
21630 if ( ! $flush && $api->is_cached( $path ) ) {
21631 $addons = self::get_all_addons();
21632
21633 return isset( $addons[ $this->_plugin->id ] ) ?
21634 $addons[ $this->_plugin->id ] :
21635 array();
21636 }
21637
21638 $result = $api->get( $path, $flush );
21639
21640 $addons = array();
21641 if ( $this->is_api_result_object( $result, 'plugins' ) &&
21642 is_array( $result->plugins )
21643 ) {
21644 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
21645 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
21646 }
21647
21648 $this->_store_addons( $addons, true );
21649 }
21650
21651 return $addons;
21652 }
21653
21654 /**
21655 * Handle user email update.
21656 *
21657 * @author Vova Feldman (@svovaf)
21658 * @since 1.0.3
21659 * @uses FS_Api
21660 *
21661 * @param string $new_email
21662 *
21663 * @return object
21664 */
21665 private function update_email( $new_email ) {
21666 $this->_logger->entrance();
21667
21668
21669 $api = $this->get_api_user_scope();
21670 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
21671 'email' => $new_email,
21672 'after_email_confirm_url' => $this->_get_admin_page_url(
21673 'account',
21674 array( 'fs_action' => 'sync_user' )
21675 ),
21676 ) );
21677
21678 if ( ! isset( $user->error ) ) {
21679 $this->_user->email = $user->email;
21680 $this->_user->is_verified = $user->is_verified;
21681 $this->_store_user();
21682 } else {
21683 // handle different error cases.
21684
21685 }
21686
21687 return $user;
21688 }
21689
21690 #----------------------------------------------------------------------------------
21691 #region API Error Handling
21692 #----------------------------------------------------------------------------------
21693
21694 /**
21695 * @author Vova Feldman (@svovaf)
21696 * @since 1.1.1
21697 *
21698 * @param mixed $result
21699 *
21700 * @return bool Is API result contains an error.
21701 */
21702 private function is_api_error( $result ) {
21703 return FS_Api::is_api_error( $result );
21704 }
21705
21706 /**
21707 * Checks if given API result is a non-empty and not an error object.
21708 *
21709 * @author Vova Feldman (@svovaf)
21710 * @since 1.2.1.5
21711 *
21712 * @param mixed $result
21713 * @param string|null $required_property Optional property we want to verify that is set.
21714 *
21715 * @return bool
21716 */
21717 function is_api_result_object( $result, $required_property = null ) {
21718 return FS_Api::is_api_result_object( $result, $required_property );
21719 }
21720
21721 /**
21722 * Checks if given API result is a non-empty entity object with non-empty ID.
21723 *
21724 * @author Vova Feldman (@svovaf)
21725 * @since 1.2.1.5
21726 *
21727 * @param mixed $result
21728 *
21729 * @return bool
21730 */
21731 private function is_api_result_entity( $result ) {
21732 return FS_Api::is_api_result_entity( $result );
21733 }
21734
21735 #endregion
21736
21737 /**
21738 * Make sure a given argument is an array of a specific type.
21739 *
21740 * @author Vova Feldman (@svovaf)
21741 * @since 1.2.1.5
21742 *
21743 * @param mixed $array
21744 * @param string $class
21745 *
21746 * @return bool
21747 */
21748 private function is_array_instanceof( $array, $class ) {
21749 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
21750 }
21751
21752 /**
21753 * Start install ownership change.
21754 *
21755 * @author Vova Feldman (@svovaf)
21756 * @since 1.1.1
21757 * @uses FS_Api
21758 *
21759 * @param string $candidate_email
21760 *
21761 * @return bool Is ownership change successfully initiated.
21762 */
21763 private function init_change_owner( $candidate_email ) {
21764 $this->_logger->entrance();
21765
21766 $api = $this->get_api_site_scope();
21767 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
21768 'email' => $candidate_email,
21769 'after_confirm_url' => $this->_get_admin_page_url(
21770 'account',
21771 array( 'fs_action' => 'change_owner' )
21772 ),
21773 ) );
21774
21775 return ! $this->is_api_error( $result );
21776 }
21777
21778 /**
21779 * Handle install ownership change.
21780 *
21781 * @author Vova Feldman (@svovaf)
21782 * @since 1.1.1
21783 * @uses FS_Api
21784 *
21785 * @return bool Was ownership change successfully complete.
21786 */
21787 private function complete_change_owner() {
21788 $this->_logger->entrance();
21789
21790 $site_result = $this->get_api_site_scope( true )->get();
21791 $site = new FS_Site( $site_result );
21792 $this->_site = $site;
21793
21794 $user = new FS_User();
21795 $user->id = fs_request_get( 'user_id' );
21796
21797 // Validate install's user and given user.
21798 if ( $user->id != $this->_site->user_id ) {
21799 return false;
21800 }
21801
21802 $user->public_key = fs_request_get( 'user_public_key' );
21803 $user->secret_key = fs_request_get( 'user_secret_key' );
21804
21805 // Fetch new user information.
21806 $this->_user = $user;
21807 $user_result = $this->get_api_user_scope( true )->get();
21808 $user = new FS_User( $user_result );
21809 $this->_user = $user;
21810
21811 $this->_set_account( $user, $site );
21812
21813 return true;
21814 }
21815
21816 /**
21817 * Completes ownership change by license.
21818 *
21819 * @author Leo Fajardo (@leorw)
21820 * @since 2.3.2
21821 *
21822 * @param number $user_id
21823 * @param array[string]number $install_ids_by_slug_map
21824 *
21825 */
21826 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
21827 $this->_logger->entrance();
21828
21829 $this->sync_user_by_current_install( $user_id );
21830
21831 $result = $this->get_api_user_scope( true )->get(
21832 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
21833 );
21834
21835 if ( $this->is_api_result_object( $result, 'installs' ) ) {
21836 $sites = self::get_all_sites( $this->get_module_type() );
21837 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
21838
21839 foreach ( $result->installs as $install ) {
21840 $site = new FS_Site( $install );
21841
21842 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
21843 }
21844
21845 $this->set_account_option( 'sites', $sites, true );
21846 }
21847 }
21848
21849 /**
21850 * Handle user name update.
21851 *
21852 * @author Vova Feldman (@svovaf)
21853 * @since 1.0.9
21854 * @uses FS_Api
21855 *
21856 * @return object
21857 */
21858 private function update_user_name() {
21859 $this->_logger->entrance();
21860 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
21861
21862 $api = $this->get_api_user_scope();
21863 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
21864 'name' => $name,
21865 ) );
21866
21867 if ( ! isset( $user->error ) ) {
21868 $this->_user->first = $user->first;
21869 $this->_user->last = $user->last;
21870 $this->_store_user();
21871 } else {
21872 // handle different error cases.
21873
21874 }
21875
21876 return $user;
21877 }
21878
21879 /**
21880 * Verify user email.
21881 *
21882 * @author Vova Feldman (@svovaf)
21883 * @since 1.0.3
21884 * @uses FS_Api
21885 */
21886 private function verify_email() {
21887 $this->_handle_account_user_sync();
21888
21889 if ( $this->_user->is_verified() ) {
21890 return;
21891 }
21892
21893 $api = $this->get_api_site_scope();
21894 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
21895 'after_email_confirm_url' => $this->_get_admin_page_url(
21896 'account',
21897 array( 'fs_action' => 'sync_user' )
21898 )
21899 ) );
21900
21901 if ( ! isset( $result->error ) ) {
21902 $this->_admin_notices->add( sprintf(
21903 $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' ),
21904 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
21905 ) );
21906 } else {
21907 // handle different error cases.
21908
21909 }
21910 }
21911
21912 /**
21913 * @author Vova Feldman (@svovaf)
21914 * @since 1.1.2
21915 *
21916 * @param array $params
21917 * @param bool|null $network
21918 *
21919 * @return string
21920 */
21921 function get_activation_url( $params = array(), $network = null ) {
21922 if ( $this->is_addon() && $this->has_free_plan() ) {
21923 /**
21924 * @author Vova Feldman (@svovaf)
21925 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
21926 */
21927 return $this->get_parent_instance()->get_activation_url( $params );
21928 }
21929
21930 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
21931 }
21932
21933 /**
21934 * @author Vova Feldman (@svovaf)
21935 * @since 1.2.1.5
21936 *
21937 * @param array $params
21938 *
21939 * @return string
21940 */
21941 function get_reconnect_url( $params = array() ) {
21942 $params['fs_action'] = 'reset_anonymous_mode';
21943 $params['fs_unique_affix'] = $this->get_unique_affix();
21944
21945 return $this->get_activation_url( $params );
21946 }
21947
21948 /**
21949 * Get the URL of the page that should be loaded after the user connect
21950 * or skip in the opt-in screen.
21951 *
21952 * @author Vova Feldman (@svovaf)
21953 * @since 1.1.3
21954 *
21955 * @param string $filter Filter name.
21956 * @param array $params Since 1.2.2.7
21957 * @param bool|null $network
21958 *
21959 * @return string
21960 */
21961 function get_after_activation_url( $filter, $params = array(), $network = null ) {
21962 if ( $this->show_opt_in_on_themes_page() &&
21963 ( fs_request_has( 'pending_activation' ) ||
21964 // For cases when the first time path is set, even though it's a WP.org theme.
21965 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
21966 ) {
21967 $first_time_path = '';
21968 } else {
21969 $first_time_path = $this->_menu->get_first_time_path(
21970 fs_is_network_admin() && $this->_is_network_active
21971 );
21972 }
21973
21974 if ( $this->_is_network_active &&
21975 fs_is_network_admin() &&
21976 ! $this->_menu->has_network_menu() &&
21977 $this->is_network_registered()
21978 ) {
21979 $target_url = $this->get_account_url();
21980 } else {
21981 // Default plugin's page.
21982 $target_url = $this->_get_admin_page_url( '', array(), $network );
21983 }
21984
21985 return add_query_arg( $params, $this->apply_filters(
21986 $filter,
21987 empty( $first_time_path ) ?
21988 $target_url :
21989 $first_time_path
21990 ) );
21991 }
21992
21993 /**
21994 * Handle account page updates / edits / actions.
21995 *
21996 * @author Vova Feldman (@svovaf)
21997 * @since 1.0.2
21998 *
21999 */
22000 private function _handle_account_edits() {
22001 if ( ! $this->is_user_admin() ) {
22002 return;
22003 }
22004
22005 $action = fs_get_action();
22006
22007 if ( empty( $action ) ) {
22008 return;
22009 }
22010
22011 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22012 $install_id = fs_request_get( 'install_id', '' );
22013
22014 // Alias.
22015 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22016
22017 $is_network_action = $this->is_network_level_action();
22018 $blog_id = $this->is_network_level_site_specific_action();
22019 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22020
22021 if ( is_numeric( $blog_id ) ) {
22022 $this->switch_to_blog( $blog_id );
22023 } else {
22024 $blog_id = '';
22025 }
22026
22027 switch ( $action ) {
22028 case 'opt_in':
22029 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22030
22031 if ( $is_parent_plugin_action ) {
22032 if ( $is_network_action && ! empty( $blog_id ) ) {
22033 if ( ! $this->is_registered() ) {
22034 $this->install_with_user(
22035 $this->get_network_user(),
22036 false,
22037 false,
22038 false,
22039 false
22040 );
22041
22042 $this->_admin_notices->add(
22043 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22044 $this->get_text_inline( 'Awesome', 'awesome' )
22045 );
22046 }
22047 }
22048 }
22049 break;
22050
22051 case 'toggle_tracking':
22052 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22053
22054 if ( $is_parent_plugin_action ) {
22055 if ( $is_network_action && ! empty( $blog_id ) ) {
22056 if ( $this->is_registered() ) {
22057 if ( $this->is_tracking_prohibited() ) {
22058 if ( $this->allow_site_tracking() ) {
22059 $this->_admin_notices->add(
22060 sprintf( $this->get_text_inline( 'We appreciate your help in making the %s better by letting us track some usage data.', 'opt-out-message-appreciation' ), $this->_module_type ),
22061 $this->get_text_inline( 'Thank you!', 'thank-you' )
22062 );
22063 }
22064 } else {
22065 if ( $this->stop_site_tracking() ) {
22066 $this->_admin_notices->add(
22067 sprintf(
22068 $this->get_text_inline( 'We will no longer be sending any usage data of %s on %s to %s.', 'opted-out-successfully' ),
22069 $this->get_plugin_title(),
22070 fs_strip_url_protocol( get_site_url( $blog_id ) ),
22071 sprintf(
22072 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22073 'https://freemius.com',
22074 'freemius.com'
22075 )
22076 )
22077 );
22078 }
22079 }
22080 }
22081 }
22082 }
22083
22084 break;
22085
22086 case 'delete_account':
22087 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22088
22089 $is_network_deletion = $is_network_action && empty( $blog_id );
22090
22091 if ( $is_parent_plugin_action ) {
22092 // Delete add-on installs if have any.
22093 $installed_addons = $this->get_installed_addons();
22094 foreach ( $installed_addons as $fs_addon ) {
22095 if ( $is_network_deletion ) {
22096 $fs_addon->delete_network_account_event();
22097 } else {
22098 $fs_addon->delete_account_event();
22099 }
22100 }
22101
22102 if ( $is_network_deletion ) {
22103 $this->delete_network_account_event();
22104 } else {
22105 $this->delete_account_event();
22106 }
22107
22108 // Clear user and site.
22109 $this->_site = null;
22110 $this->_user = null;
22111
22112 $this->maybe_set_slug_and_network_menu_exists_flag();
22113
22114 fs_redirect( $this->get_activation_url() );
22115 } else {
22116 if ( $this->is_addon_activated( $plugin_id ) ) {
22117 $fs_addon = self::get_instance_by_id( $plugin_id );
22118
22119 if ( $is_network_deletion ) {
22120 $fs_addon->delete_network_account_event();
22121 } else {
22122 $fs_addon->delete_account_event();
22123 }
22124
22125 fs_redirect( $this->_get_admin_page_url( 'account' ) );
22126 }
22127 }
22128
22129 return;
22130
22131 case 'downgrade_account':
22132 if ( is_numeric( $blog_id ) ) {
22133 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22134 } else {
22135 check_admin_referer( $action );
22136 }
22137
22138 $switch_to_network_install_blog_after_cancellation = (
22139 is_numeric( $blog_id ) &&
22140 $plugin_id == $this->get_id() &&
22141 ! $this->is_trial()
22142 );
22143
22144 $result = $this->cancel_subscription_or_trial( $plugin_id );
22145 if ( $this->is_api_error( $result ) ) {
22146 $this->_admin_notices->add(
22147 $result->error->message,
22148 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22149 'error'
22150 );
22151 }
22152
22153 if ( $switch_to_network_install_blog_after_cancellation ) {
22154 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22155 }
22156
22157 return;
22158
22159 case 'activate_license':
22160 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22161
22162 $fs = $this;
22163 if ( $plugin_id != $this->get_id() ) {
22164 $fs = $this->is_addon_activated( $plugin_id ) ?
22165 self::get_instance_by_id( $plugin_id ) :
22166 null;
22167 }
22168
22169 if ( is_object( $fs ) ) {
22170 $fs->_activate_license();
22171
22172 /**
22173 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
22174 *
22175 * @author Leo Fajardo (@leorw)
22176 * @since 2.4.0
22177 */
22178 unset( $_REQUEST['plugin_id'] );
22179
22180 if ( $this->is_bundle_license_auto_activation_enabled() ) {
22181 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
22182 }
22183 }
22184
22185 return;
22186
22187 case 'deactivate_license':
22188 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22189
22190 if ( $plugin_id == $this->get_id() ) {
22191 $this->_deactivate_license();
22192
22193 if ( $this->is_only_premium() ) {
22194 // Clear user and site.
22195 $this->_site = null;
22196 $this->_user = null;
22197
22198 if ( ! $is_network_action ) {
22199 fs_redirect( $this->get_activation_url() );
22200 } else if ( is_numeric( $blog_id ) ) {
22201 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22202 }
22203 }
22204 } else {
22205 if ( $this->is_addon_activated( $plugin_id ) ) {
22206 $fs_addon = self::get_instance_by_id( $plugin_id );
22207 $fs_addon->_deactivate_license();
22208 }
22209 }
22210
22211 return;
22212
22213 case 'check_updates':
22214 check_admin_referer( $action );
22215 $this->check_updates();
22216
22217 return;
22218
22219 case 'change_owner':
22220 $state = fs_request_get( 'state', 'init' );
22221 switch ( $state ) {
22222 case 'init':
22223 $candidate_email = fs_request_get( 'candidate_email', '' );
22224
22225 if ( $this->init_change_owner( $candidate_email ) ) {
22226 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Please check your mailbox, you should receive an email via %s to confirm the ownership change. From security reasons, you must confirm the change within the next 15 min. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '<b>' . $this->_user->email . '</b>' ) );
22227 }
22228 break;
22229 case 'owner_confirmed':
22230 $candidate_email = fs_request_get( 'candidate_email', '' );
22231
22232 $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>' ) );
22233 break;
22234 case 'candidate_confirmed':
22235 if ( $this->complete_change_owner() ) {
22236 $this->_admin_notices->add_sticky(
22237 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
22238 'ownership_changed',
22239 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
22240 );
22241 } else {
22242 // @todo Handle failed ownership change message.
22243 }
22244 break;
22245 }
22246
22247 return;
22248
22249 case 'update_email':
22250 check_admin_referer( 'update_email' );
22251
22252 $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' );
22253 $result = $this->update_email( $new_email );
22254
22255 if ( isset( $result->error ) ) {
22256 switch ( $result->error->code ) {
22257 case 'user_exist':
22258 $this->_admin_notices->add(
22259 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
22260 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 . '</b>' ) .
22261 sprintf(
22262 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
22263 $this->get_account_url( 'change_owner', array(
22264 'state' => 'init',
22265 'candidate_email' => $new_email
22266 ) ),
22267 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
22268 ),
22269 $oops_text,
22270 'error'
22271 );
22272 break;
22273 }
22274 } else {
22275 $this->_admin_notices->add( $this->get_text_inline( 'Your email was successfully updated. You should receive an email with confirmation instructions in few moments.', 'email-updated-message' ) );
22276 }
22277
22278 return;
22279
22280 case 'update_user_name':
22281 check_admin_referer( 'update_user_name' );
22282
22283 $result = $this->update_user_name();
22284
22285 if ( isset( $result->error ) ) {
22286 $this->_admin_notices->add(
22287 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
22288 $oops_text,
22289 'error'
22290 );
22291 } else {
22292 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
22293 }
22294
22295 return;
22296
22297 #region Actions that might be called from external links (e.g. email)
22298
22299 case 'cancel_trial':
22300 $result = $this->cancel_subscription_or_trial( $plugin_id );
22301 if ( $this->is_api_error( $result ) ) {
22302 $this->_admin_notices->add(
22303 $result->error->message,
22304 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22305 'error'
22306 );
22307 }
22308
22309 return;
22310
22311 case 'verify_email':
22312 $this->verify_email();
22313
22314 return;
22315
22316 case 'sync_user':
22317 $this->_handle_account_user_sync();
22318
22319 return;
22320
22321 case $this->get_unique_affix() . '_sync_license':
22322 $this->_sync_license();
22323
22324 return;
22325
22326 case 'download_latest':
22327 $this->download_latest_directly( $plugin_id );
22328
22329 return;
22330
22331 #endregion
22332 }
22333
22334 if ( WP_FS__IS_POST_REQUEST ) {
22335 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
22336 foreach ( $properties as $p ) {
22337 if ( 'update_' . $p === $action ) {
22338 check_admin_referer( $action );
22339
22340 $this->_logger->log( $action );
22341
22342 $site_property = substr( $p, strlen( 'site_' ) );
22343 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
22344 $this->get_site()->{$site_property} = $site_property_value;
22345
22346 // Store account after modification.
22347 $this->_store_site();
22348
22349 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
22350
22351 $this->_admin_notices->add( sprintf(
22352 /* translators: %s: User's account property (e.g. email address, name) */
22353 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
22354 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
22355 ) );
22356
22357 return;
22358 }
22359 }
22360 }
22361 }
22362
22363 /**
22364 * Account page resources load.
22365 *
22366 * @author Vova Feldman (@svovaf)
22367 * @since 1.0.6
22368 */
22369 function _account_page_load() {
22370 $this->_logger->entrance();
22371
22372 $this->_logger->info( var_export( $_REQUEST, true ) );
22373
22374 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
22375
22376 if ( $this->has_addons() ) {
22377 wp_enqueue_script( 'plugin-install' );
22378 add_thickbox();
22379
22380 function fs_addons_body_class( $classes ) {
22381 $classes .= ' plugins-php';
22382
22383 return $classes;
22384 }
22385
22386 add_filter( 'admin_body_class', 'fs_addons_body_class' );
22387 }
22388
22389 if ( $this->has_paid_plan() &&
22390 ! $this->has_any_license() &&
22391 ! $this->is_sync_executed() &&
22392 $this->is_tracking_allowed()
22393 ) {
22394 /**
22395 * If no licenses found and no sync job was executed during the last 24 hours,
22396 * just execute the sync job right away (blocking execution).
22397 *
22398 * @since 1.1.7.3
22399 */
22400 $this->run_manual_sync();
22401 }
22402
22403 $this->_handle_account_edits();
22404
22405 if (
22406 is_object( $this->_license ) &&
22407 $this->_license->user_id == $this->_user->id &&
22408 ! $this->is_whitelabeled( true )
22409 ) {
22410 $this->_admin_notices->add(
22411 sprintf(
22412 $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' ),
22413 sprintf(
22414 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
22415 $this->get_text_inline( 'Click here', 'click-here' )
22416 )
22417 ),
22418 '',
22419 'success',
22420 false,
22421 'license_not_whitelabeled'
22422 );
22423 }
22424
22425 $this->do_action( 'account_page_load_before_departure' );
22426 }
22427
22428 /**
22429 * Renders the "Affiliation" page.
22430 *
22431 * @author Leo Fajardo (@leorw)
22432 * @since 1.2.3
22433 */
22434 function _affiliation_page_render() {
22435 $this->_logger->entrance();
22436
22437 $this->fetch_affiliate_and_terms();
22438
22439 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
22440
22441 $vars = array( 'id' => $this->_module_id );
22442 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
22443 }
22444
22445
22446 /**
22447 * Render account page.
22448 *
22449 * @author Vova Feldman (@svovaf)
22450 * @since 1.0.0
22451 */
22452 function _account_page_render() {
22453 $this->_logger->entrance();
22454
22455 $template = 'account.php';
22456 $vars = array( 'id' => $this->_module_id );
22457
22458 /**
22459 * Added filter to the template to allow developers wrapping the template
22460 * in custom HTML (e.g. within a wizard/tabs).
22461 *
22462 * @author Vova Feldman (@svovaf)
22463 * @since 1.2.1.6
22464 */
22465 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
22466 }
22467
22468 /**
22469 * Render account connect page.
22470 *
22471 * @author Vova Feldman (@svovaf)
22472 * @since 1.0.7
22473 */
22474 function _connect_page_render() {
22475 $this->_logger->entrance();
22476
22477 $vars = array( 'id' => $this->_module_id );
22478
22479 /**
22480 * Added filter to the template to allow developers wrapping the template
22481 * in custom HTML (e.g. within a wizard/tabs).
22482 *
22483 * @author Vova Feldman (@svovaf)
22484 * @since 1.2.1.6
22485 */
22486 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
22487 }
22488
22489 /**
22490 * Load required resources before add-ons page render.
22491 *
22492 * @author Vova Feldman (@svovaf)
22493 * @since 1.0.6
22494 */
22495 function _addons_page_load() {
22496 $this->_logger->entrance();
22497
22498 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
22499
22500 wp_enqueue_script( 'plugin-install' );
22501 add_thickbox();
22502
22503 function fs_addons_body_class( $classes ) {
22504 $classes .= ' plugins-php';
22505
22506 return $classes;
22507 }
22508
22509 add_filter( 'admin_body_class', 'fs_addons_body_class' );
22510
22511 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
22512 $this->_admin_notices->add(
22513 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>' ),
22514 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
22515 'update-nag'
22516 );
22517 }
22518 }
22519
22520 /**
22521 * Render add-ons page.
22522 *
22523 * @author Vova Feldman (@svovaf)
22524 * @since 1.0.6
22525 */
22526 function _addons_page_render() {
22527 $this->_logger->entrance();
22528
22529 $vars = array( 'id' => $this->_module_id );
22530
22531 /**
22532 * Added filter to the template to allow developers wrapping the template
22533 * in custom HTML (e.g. within a wizard/tabs).
22534 *
22535 * @author Vova Feldman (@svovaf)
22536 * @since 1.2.1.6
22537 */
22538 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
22539 }
22540
22541 /* Pricing & Upgrade
22542 ------------------------------------------------------------------------------------------------------------------*/
22543 /**
22544 * Render pricing page.
22545 *
22546 * @author Vova Feldman (@svovaf)
22547 * @since 1.0.0
22548 */
22549 function _pricing_page_render() {
22550 $this->_logger->entrance();
22551
22552 $vars = array( 'id' => $this->_module_id );
22553
22554 if ( 'true' === fs_request_get( 'checkout', false ) ) {
22555 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
22556 } else {
22557 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
22558 }
22559 }
22560
22561 /**
22562 * @author Leo Fajardo (@leorw)
22563 * @since 2.3.1
22564 */
22565 function _maybe_add_pricing_ajax_handler() {
22566 if ( ! $this->should_use_external_pricing() ) {
22567 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
22568 }
22569 }
22570
22571 /**
22572 * @author Leo Fajardo (@leorw)
22573 * @since 2.3.1
22574 */
22575 function _fs_pricing_ajax_action_handler() {
22576 $this->check_ajax_referer( 'pricing_ajax_action' );
22577
22578 $result = null;
22579 $pricing_action = fs_request_get( 'pricing_action' );
22580
22581 switch ( $pricing_action ) {
22582 case 'fetch_pricing_data':
22583 $params = array(
22584 'is_enriched' => true,
22585 'trial' => fs_request_get_bool( 'trial' ),
22586 'sandbox' => fs_request_get( 'sandbox' ),
22587 's_ctx_type' => fs_request_get( 's_ctx_type' ),
22588 's_ctx_id' => fs_request_get( 's_ctx_id' ),
22589 's_ctx_ts' => fs_request_get( 's_ctx_ts' ),
22590 's_ctx_secure' => fs_request_get( 's_ctx_secure' ),
22591 );
22592
22593 $bundle_id = $this->get_bundle_id();
22594 $bundle_public_key = $this->get_bundle_public_key();
22595
22596 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
22597
22598 if ( ! $has_bundle_context ) {
22599 $api = $this->get_api_plugin_scope();
22600 } else {
22601 $api = FS_Api::instance(
22602 $bundle_id,
22603 'plugin',
22604 $bundle_id,
22605 $bundle_public_key,
22606 ! $this->is_live(),
22607 false,
22608 $this->get_sdk_version()
22609 );
22610
22611 $params['plugin_id'] = $this->get_id();
22612 $params['plugin_public_key'] = $this->get_public_key();
22613 }
22614
22615 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
22616 break;
22617 case 'start_trial':
22618 $result = $this->opt_in(
22619 false,
22620 false,
22621 false,
22622 false,
22623 false,
22624 fs_request_get( 'plan_id' )
22625 );
22626 }
22627
22628 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
22629 $this->_logger->api_error( $result );
22630
22631 self::shoot_ajax_failure(
22632 isset( $result->error ) ?
22633 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
22634 var_export( $result, true )
22635 );
22636 }
22637
22638 $this->shoot_ajax_success( $result );
22639 }
22640
22641 #----------------------------------------------------------------------------------
22642 #region Contact Us
22643 #----------------------------------------------------------------------------------
22644
22645 /**
22646 * Render contact-us page.
22647 *
22648 * @author Vova Feldman (@svovaf)
22649 * @since 1.0.3
22650 */
22651 function _contact_page_render() {
22652 $this->_logger->entrance();
22653
22654 $vars = array( 'id' => $this->_module_id );
22655
22656 /**
22657 * Added filter to the template to allow developers wrapping the template
22658 * in custom HTML (e.g. within a wizard/tabs).
22659 *
22660 * @author Vova Feldman (@svovaf)
22661 * @since 2.1.3
22662 */
22663 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
22664 }
22665
22666 #endregion ------------------------------------------------------------------------
22667
22668 /**
22669 * Hide all admin notices to prevent distractions.
22670 *
22671 * @author Vova Feldman (@svovaf)
22672 * @since 1.0.3
22673 *
22674 * @uses remove_all_actions()
22675 */
22676 private static function _hide_admin_notices() {
22677 remove_all_actions( 'admin_notices' );
22678 remove_all_actions( 'network_admin_notices' );
22679 remove_all_actions( 'all_admin_notices' );
22680 remove_all_actions( 'user_admin_notices' );
22681 }
22682
22683 static function _clean_admin_content_section_hook() {
22684 self::_hide_admin_notices();
22685
22686 // Hide footer.
22687 echo '<style>#wpfooter { display: none !important; }</style>';
22688 }
22689
22690 /**
22691 * Attach to admin_head hook to hide all admin notices.
22692 *
22693 * @author Vova Feldman (@svovaf)
22694 * @since 1.0.3
22695 */
22696 static function _clean_admin_content_section() {
22697 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
22698 }
22699
22700 /* CSS & JavaScript
22701 ------------------------------------------------------------------------------------------------------------------*/
22702 /* function _enqueue_script($handle, $src) {
22703 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
22704
22705 $this->_logger->entrance( 'script = ' . $url );
22706
22707 wp_enqueue_script( $handle, $url );
22708 }*/
22709
22710 /* SDK
22711 ------------------------------------------------------------------------------------------------------------------*/
22712 private $_user_api;
22713
22714 /**
22715 *
22716 * @author Vova Feldman (@svovaf)
22717 * @since 1.0.2
22718 *
22719 * @param bool $flush
22720 *
22721 * @return FS_Api
22722 */
22723 private function get_api_user_scope( $flush = false ) {
22724 if ( ! isset( $this->_user_api ) || $flush ) {
22725 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
22726 }
22727
22728 return $this->_user_api;
22729 }
22730
22731 /**
22732 * @author Vova Feldman (@svovaf)
22733 * @since 2.0.0
22734 *
22735 * @param \FS_User $user
22736 *
22737 * @return \FS_Api
22738 */
22739 private function get_api_user_scope_by_user( FS_User $user ) {
22740 return FS_Api::instance(
22741 $this->_module_id,
22742 'user',
22743 $user->id,
22744 $user->public_key,
22745 ! $this->is_live(),
22746 $user->secret_key,
22747 $this->get_sdk_version()
22748 );
22749 }
22750
22751 /**
22752 *
22753 * @author Leo Fajardo (@leorw)
22754 * @since 2.0.0
22755 *
22756 * @param bool $flush
22757 *
22758 * @return FS_Api
22759 */
22760 private function get_current_or_network_user_api_scope( $flush = false ) {
22761 if ( ! $this->_is_network_active ||
22762 ( isset( $this->_user ) && $this->_user instanceof FS_User )
22763 ) {
22764 return $this->get_api_user_scope( $flush );
22765 }
22766
22767 $user = $this->get_current_or_network_user();
22768
22769 $this->_user_api = FS_Api::instance(
22770 $this->_module_id,
22771 'user',
22772 $user->id,
22773 $user->public_key,
22774 ! $this->is_live(),
22775 $user->secret_key,
22776 $this->get_sdk_version()
22777 );
22778
22779 return $this->_user_api;
22780 }
22781
22782 private $_site_api;
22783
22784 /**
22785 *
22786 * @author Vova Feldman (@svovaf)
22787 * @since 1.0.2
22788 *
22789 * @param bool $flush
22790 *
22791 * @return FS_Api
22792 */
22793 private function get_api_site_scope( $flush = false ) {
22794 if ( ! isset( $this->_site_api ) || $flush ) {
22795 $this->_site_api = FS_Api::instance(
22796 $this->_module_id,
22797 'install',
22798 $this->_site->id,
22799 $this->_site->public_key,
22800 ! $this->is_live(),
22801 $this->_site->secret_key,
22802 $this->get_sdk_version()
22803 );
22804 }
22805
22806 return $this->_site_api;
22807 }
22808
22809 private $_plugin_api;
22810
22811 /**
22812 * Get plugin public API scope.
22813 *
22814 * @author Vova Feldman (@svovaf)
22815 * @since 1.0.7
22816 *
22817 * @return FS_Api
22818 */
22819 function get_api_plugin_scope() {
22820 if ( ! isset( $this->_plugin_api ) ) {
22821 $this->_plugin_api = FS_Api::instance(
22822 $this->_module_id,
22823 'plugin',
22824 $this->_plugin->id,
22825 $this->_plugin->public_key,
22826 ! $this->is_live(),
22827 false,
22828 $this->get_sdk_version()
22829 );
22830 }
22831
22832 return $this->_plugin_api;
22833 }
22834
22835 /**
22836 * Get bundle public API scope.
22837 *
22838 * @author Vova Feldman (@svovaf)
22839 * @since 2.3.1
22840 *
22841 * @return FS_Api
22842 */
22843 function get_api_bundle_scope() {
22844 return FS_Api::instance(
22845 $this->get_bundle_id(),
22846 'plugin',
22847 $this->get_bundle_id(),
22848 $this->get_bundle_public_key(),
22849 ! $this->is_live(),
22850 false,
22851 $this->get_sdk_version()
22852 );
22853 }
22854
22855 /**
22856 * Get site API scope object (fallback to public plugin scope when not registered).
22857 *
22858 * @author Vova Feldman (@svovaf)
22859 * @since 1.0.7
22860 *
22861 * @return FS_Api
22862 */
22863 function get_api_site_or_plugin_scope() {
22864 return $this->is_registered() ?
22865 $this->get_api_site_scope() :
22866 $this->get_api_plugin_scope();
22867 }
22868
22869 /**
22870 * @author Leo Fajardo (@leorw)
22871 * @since 2.2.3.1
22872 *
22873 * @param object $result
22874 */
22875 private function maybe_modify_api_curl_error_message( $result ) {
22876 if (
22877 'cUrlMissing' !== $result->error->type &&
22878 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
22879 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
22880 ) {
22881 return;
22882 }
22883
22884 $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' ) .
22885 ' ' .
22886 $this->esc_html_inline(
22887 sprintf(
22888 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
22889 implode(
22890 ', ',
22891 $this->apply_filters( 'api_domains', array(
22892 'api.freemius.com',
22893 'wp.freemius.com'
22894 ) )
22895 )
22896 ),
22897 'connectivity-whitelist'
22898 ) .
22899 ' ' .
22900 sprintf(
22901 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
22902 $this->get_module_type()
22903 );
22904 }
22905
22906 /**
22907 * Show trial promotional notice (if any trial exist).
22908 *
22909 * @author Vova Feldman (@svovaf)
22910 * @since 1.0.9
22911 *
22912 * @param FS_Plugin_Plan[] $plans
22913 */
22914 function _check_for_trial_plans( $plans ) {
22915 /**
22916 * 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.
22917 *
22918 * @author Vova Feldman (@svovaf)
22919 * @since 2.1.2
22920 */
22921 if ( ! is_array( $plans ) && is_object( $plans ) ) {
22922 $plans = array( $plans );
22923 }
22924
22925 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
22926 $plans = array();
22927 }
22928
22929 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
22930 }
22931
22932 /**
22933 * During trial promotion the "upgrade" submenu item turns to
22934 * "start trial" to encourage the trial. Since we want to keep
22935 * the same menu item handler and there's no robust way to
22936 * add new arguments to the menu item link's querystring,
22937 * use JavaScript to find the menu item and update the href of
22938 * the link.
22939 *
22940 * @author Vova Feldman (@svovaf)
22941 * @since 1.2.1.5
22942 */
22943 function _fix_start_trial_menu_item_url() {
22944 $template_args = array( 'id' => $this->_module_id );
22945 fs_require_template( 'add-trial-to-pricing.php', $template_args );
22946 }
22947
22948 /**
22949 * Check if module is currently in a trial promotion mode.
22950 *
22951 * @author Vova Feldman (@svovaf)
22952 * @since 1.2.2.7
22953 *
22954 * @return bool
22955 */
22956 function is_in_trial_promotion() {
22957 return $this->_admin_notices->has_sticky( 'trial_promotion' );
22958 }
22959
22960 /**
22961 * Show trial promotional notice (if any trial exist).
22962 *
22963 * @author Vova Feldman (@svovaf)
22964 * @since 1.0.9
22965 *
22966 * @return bool If trial notice added.
22967 */
22968 function _add_trial_notice() {
22969 if ( ! $this->is_user_admin() ) {
22970 return false;
22971 }
22972
22973 if ( ! $this->is_user_in_admin() ) {
22974 return false;
22975 }
22976
22977 if ( $this->_is_network_active ) {
22978 if ( fs_is_network_admin() ) {
22979 // Network level trial is disabled at the moment.
22980 return false;
22981 }
22982
22983 if ( ! $this->is_delegated_connection() ) {
22984 // Only delegated sites should support trials.
22985 return false;
22986 }
22987 }
22988
22989 // Check if trial message is already shown.
22990 if ( $this->is_in_trial_promotion() ) {
22991 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
22992
22993 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
22994
22995 return false;
22996 }
22997
22998 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
22999 // Don't show trial if running the premium code, unless running in DEV mode.
23000 return false;
23001 }
23002
23003 if ( ! $this->has_trial_plan() ) {
23004 // No plans with trial.
23005 return false;
23006 }
23007
23008 if ( ! $this->apply_filters( 'show_trial', true ) ) {
23009 // Developer explicitly asked not to show the trial promo.
23010 return false;
23011 }
23012
23013 if ( $this->is_registered() ) {
23014 // Check if trial already utilized.
23015 if ( $this->_site->is_trial_utilized() ) {
23016 return false;
23017 }
23018
23019 if ( $this->is_paying_or_trial() ) {
23020 // Don't show trial if paying or already in trial.
23021 return false;
23022 }
23023 }
23024
23025 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23026 // If not yet opted-in/skipped, or pending activation, don't show trial.
23027 return false;
23028 }
23029
23030 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
23031 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
23032
23033 // Show promotion if never shown before and 24 hours after initial activation with FS.
23034 if ( ! $was_promotion_shown_before &&
23035 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
23036 ) {
23037 return false;
23038 }
23039
23040 // OR if promotion was shown before, try showing it every 30 days.
23041 if ( $was_promotion_shown_before &&
23042 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
23043 ) {
23044 return false;
23045 }
23046
23047 $trial_period = $this->_trial_days;
23048 $require_payment = $this->_is_trial_require_payment;
23049 $trial_url = $this->get_trial_url();
23050 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
23051
23052 if ( $this->is_registered() ) {
23053 // If opted-in, override trial with up to date data from API.
23054 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
23055 $trial_plans_count = count( $trial_plans );
23056
23057 if ( 0 === $trial_plans_count ) {
23058 // If there's no plans with a trial just exit.
23059 return false;
23060 }
23061
23062 /**
23063 * @var FS_Plugin_Plan $paid_plan
23064 */
23065 $paid_plan = $trial_plans[0];
23066 $require_payment = $paid_plan->is_require_subscription;
23067 $trial_period = $paid_plan->trial_period;
23068
23069 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
23070
23071 if ( $total_paid_plans !== $trial_plans_count ) {
23072 // Not all paid plans have a trial - generate a string of those that have it.
23073 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
23074 $plans_string .= sprintf(
23075 ' <a href="%s">%s</a>',
23076 $trial_url,
23077 $trial_plans[ $i ]->title
23078 );
23079
23080 if ( $i < $trial_plans_count - 2 ) {
23081 $plans_string .= ', ';
23082 } else if ( $i == $trial_plans_count - 2 ) {
23083 $plans_string .= ' and ';
23084 }
23085 }
23086 }
23087 }
23088
23089 $message = sprintf(
23090 $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' ),
23091 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
23092 $plans_string,
23093 $trial_period
23094 );
23095
23096 // "No Credit-Card Required" or "No Commitment for N Days".
23097 $cc_string = $require_payment ?
23098 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
23099 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
23100
23101
23102 // Start trial button.
23103 $button = ' ' . sprintf(
23104 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
23105 $trial_url,
23106 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
23107 );
23108
23109 $this->_admin_notices->add_sticky(
23110 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
23111 'trial_promotion',
23112 '',
23113 'promotion'
23114 );
23115
23116 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
23117
23118 return true;
23119 }
23120
23121 /**
23122 * Lets users/customers know that the product has an affiliate program.
23123 *
23124 * @author Leo Fajardo (@leorw)
23125 * @since 1.2.2.11
23126 *
23127 * @return bool Returns true if the notice has been added.
23128 */
23129 function _add_affiliate_program_notice() {
23130 if ( ! $this->is_user_admin() ) {
23131 return false;
23132 }
23133
23134 if ( ! $this->is_user_in_admin() ) {
23135 return false;
23136 }
23137
23138 // Check if the notice is already shown.
23139 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
23140 return false;
23141 }
23142
23143 if (
23144 // Product has no affiliate program.
23145 ! $this->has_affiliate_program() ||
23146 // User has applied for an affiliate account.
23147 ! empty( $this->_storage->affiliate_application_data )
23148 ) {
23149 return false;
23150 }
23151
23152 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
23153 // Developer explicitly asked not to show the notice about the affiliate program.
23154 return false;
23155 }
23156
23157 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23158 // If not yet opted in/skipped, or pending activation, don't show the notice.
23159 return false;
23160 }
23161
23162 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
23163 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
23164
23165 /**
23166 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
23167 * activation with FS.
23168 */
23169 if ( $was_notice_shown_before ||
23170 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
23171 ) {
23172 return false;
23173 }
23174
23175 if ( ! $this->is_paying() &&
23176 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
23177 ) {
23178 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
23179 return false;
23180 }
23181
23182 $message = sprintf(
23183 $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' ),
23184 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
23185 $this->get_module_label( true )
23186 );
23187
23188 // HTML code for the "Learn more..." button.
23189 $button = ' ' . sprintf(
23190 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
23191 $this->_get_admin_page_url( 'affiliation' ),
23192 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
23193 );
23194
23195 $this->_admin_notices->add_sticky(
23196 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
23197 'affiliate_program',
23198 '',
23199 'promotion'
23200 );
23201
23202 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
23203
23204 return true;
23205 }
23206
23207 /**
23208 * @author Vova Feldman (@svovaf)
23209 * @since 1.2.1.5
23210 */
23211 function _enqueue_common_css() {
23212 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
23213 // Add basic CSS for admin-notices and menu-item colors.
23214 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23215 }
23216 }
23217
23218 /**
23219 * @author Leo Fajardo (@leorw)
23220 * @since 1.2.2
23221 */
23222 function _show_theme_activation_optin_dialog() {
23223 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
23224
23225 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
23226 }
23227
23228 /**
23229 * @author Leo Fajardo (@leorw)
23230 * @since 1.2.2
23231 */
23232 function _add_fs_theme_activation_dialog() {
23233 global $pagenow;
23234
23235 if ( 'themes.php' !== $pagenow ) {
23236 return;
23237 }
23238
23239 $vars = array( 'id' => $this->_module_id );
23240 fs_require_once_template( 'connect.php', $vars );
23241 }
23242
23243 /* Action Links
23244 ------------------------------------------------------------------------------------------------------------------*/
23245 private $_action_links_hooked = false;
23246 private $_action_links = array();
23247
23248 /**
23249 * Hook to plugin action links filter.
23250 *
23251 * @author Vova Feldman (@svovaf)
23252 * @since 1.0.0
23253 */
23254 private function hook_plugin_action_links() {
23255 $this->_logger->entrance();
23256
23257 $this->_action_links_hooked = true;
23258
23259 $this->_logger->log( 'Adding action links hooks.' );
23260
23261 // Add action link to settings page.
23262 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
23263 &$this,
23264 '_modify_plugin_action_links_hook'
23265 ), WP_FS__DEFAULT_PRIORITY, 2 );
23266 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
23267 &$this,
23268 '_modify_plugin_action_links_hook'
23269 ), WP_FS__DEFAULT_PRIORITY, 2 );
23270 }
23271
23272 /**
23273 * Add plugin action link.
23274 *
23275 * @author Vova Feldman (@svovaf)
23276 * @since 1.0.0
23277 *
23278 * @param $label
23279 * @param $url
23280 * @param bool $external
23281 * @param int $priority
23282 * @param bool $key
23283 */
23284 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
23285 $this->_logger->entrance();
23286
23287 if ( ! isset( $this->_action_links[ $priority ] ) ) {
23288 $this->_action_links[ $priority ] = array();
23289 }
23290
23291 if ( false === $key ) {
23292 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
23293 }
23294
23295 $this->_action_links[ $priority ][] = array(
23296 'label' => $label,
23297 'href' => $url,
23298 'key' => $key,
23299 'external' => $external
23300 );
23301 }
23302
23303 /**
23304 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
23305 *
23306 * @author Vova Feldman (@svovaf)
23307 * @since 1.0.0
23308 */
23309 function _add_upgrade_action_link() {
23310 $this->_logger->entrance();
23311
23312 $is_activation_mode = $this->is_activation_mode();
23313
23314 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
23315
23316 /**
23317 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
23318 * and "Add-Ons" menus should be added.
23319 *
23320 * @author Leo Fajardo (@leorw)
23321 * @since 2.3.0
23322 */
23323 $add_upgrade_link = (
23324 $add_action_links ||
23325 ( $is_activation_mode && $this->is_only_premium() )
23326 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
23327
23328 $add_addons_link = ( $add_action_links && $this->has_addons() );
23329
23330 if ( ! $add_upgrade_link && ! $add_addons_link ) {
23331 return;
23332 }
23333
23334 if (
23335 $add_upgrade_link &&
23336 $this->is_pricing_page_visible() &&
23337 $this->is_submenu_item_visible( 'pricing' )
23338 ) {
23339 $this->add_plugin_action_link(
23340 $this->get_text_inline( 'Upgrade', 'upgrade' ),
23341 $this->get_upgrade_url(),
23342 false,
23343 7,
23344 'upgrade'
23345 );
23346 }
23347
23348 if (
23349 $add_addons_link &&
23350 $this->has_addons() &&
23351 $this->is_submenu_item_visible( 'addons' )
23352 ) {
23353 $this->add_plugin_action_link(
23354 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
23355 $this->_get_admin_page_url( 'addons' ),
23356 false,
23357 9,
23358 'addons'
23359 );
23360 }
23361 }
23362
23363 /**
23364 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
23365 *
23366 * @author Leo Fajardo (@leorw)
23367 * @since 1.1.9
23368 */
23369 function _add_license_action_link() {
23370 $this->_logger->entrance();
23371
23372 if ( ! self::is_ajax() ) {
23373 // Inject license activation dialog UI and client side code.
23374 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
23375 }
23376
23377 $link_text = $this->is_free_plan() ?
23378 $this->get_text_inline( 'Activate License', 'activate-license' ) :
23379 $this->get_text_inline( 'Change License', 'change-license' );
23380
23381 $this->add_plugin_action_link(
23382 $link_text,
23383 '#',
23384 false,
23385 11,
23386 ( 'activate-license ' . $this->get_unique_affix() )
23387 );
23388 }
23389
23390 /**
23391 * @author Leo Fajardo (@leorw)
23392 * @since 2.0.2
23393 */
23394 function _add_premium_version_upgrade_selection_action() {
23395 $this->_logger->entrance();
23396
23397 if ( ! self::is_ajax() ) {
23398 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
23399 }
23400 }
23401
23402 /**
23403 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
23404 *
23405 * @author Leo Fajardo (@leorw)
23406 * @since 1.2.1.5
23407 */
23408 function _add_tracking_links() {
23409 if ( ! current_user_can( 'manage_options' ) ) {
23410 return;
23411 }
23412
23413 $this->_logger->entrance();
23414
23415 /**
23416 * @author Vova Feldman (@svovaf)
23417 * @since 2.3.2 Allow opting out from usage-tracking for paid products too by giving the appropriate warning letting the user know the automatic updates mechanism cannot function without an ongoing connection to the licensing and updates engine.
23418 */
23419 /*if ( $this->is_premium() ) {
23420 // Don't add opt-in/out for premium code base.
23421 return;
23422 }*/
23423
23424 if ( $this->is_only_premium() && $this->is_free_plan() ) {
23425 // 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.
23426 return;
23427 }
23428
23429 if (
23430 $this->is_addon() &&
23431 ! $this->is_only_premium() &&
23432 $this->_parent->is_anonymous()
23433 ) {
23434 return;
23435 }
23436
23437 if ( fs_is_network_admin() ) {
23438 if ( ! $this->_is_network_active ) {
23439 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
23440 return;
23441 } else if ( $this->is_network_delegated_connection() ) {
23442 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
23443 return;
23444 }
23445 } else {
23446 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
23447 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
23448 return;
23449 }
23450 }
23451
23452 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
23453 if ( ! $this->is_registered() && $this->is_anonymous() ) {
23454 $this->connect_again();
23455
23456 return;
23457 }
23458 }
23459
23460 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
23461 ( $this->is_theme() && ! self::is_themes_page() )
23462 ) {
23463 // Only show tracking links on the plugins and themes pages.
23464 return;
23465 }
23466
23467 if (
23468 $this->is_activation_mode() &&
23469 $this->is_premium() &&
23470 ! $this->is_registered()
23471 ) {
23472 // If not yet registered and running the premium code base, a license activation link will already be shown.
23473 return;
23474 }
23475
23476 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23477 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
23478 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
23479 return;
23480 }
23481 }
23482
23483 if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) {
23484 return;
23485 }
23486
23487 if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) {
23488 return;
23489 }
23490
23491 if ( $this->add_ajax_action( 'update_tracking_permission', array( &$this, '_update_tracking_permission_callback' ) ) ) {
23492 return;
23493 }
23494
23495 $link_text_id = '';
23496 $url = '#';
23497
23498 if ( $this->is_registered() ) {
23499 if ( $this->is_tracking_allowed() ) {
23500 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
23501 } else {
23502 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
23503 }
23504 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
23505 /**
23506 * Show opt-in link only if skipped or in activation mode.
23507 */
23508 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
23509
23510 $params = ! $this->is_anonymous() ?
23511 array() :
23512 array(
23513 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
23514 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
23515 );
23516
23517 $url = $this->get_activation_url( $params );
23518 }
23519
23520 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
23521
23522 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
23523 $this->add_plugin_action_link(
23524 $link_text_id,
23525 $url,
23526 false,
23527 13,
23528 "opt-in-or-opt-out {$this->_slug}"
23529 );
23530 }
23531 }
23532
23533 /**
23534 * Get the URL of the page that should be loaded right after the plugin activation.
23535 *
23536 * @author Vova Feldman (@svovaf)
23537 * @since 1.1.7.4
23538 *
23539 * @return string
23540 */
23541 function get_after_plugin_activation_redirect_url() {
23542 $url = false;
23543
23544 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
23545 $first_time_path = $this->_menu->get_first_time_path(
23546 fs_is_network_admin() && $this->_is_network_active
23547 );
23548
23549 if ( $this->is_activation_mode() ) {
23550 $url = $this->get_activation_url();
23551 } else if ( ! empty( $first_time_path ) ) {
23552 $url = $first_time_path;
23553 } else {
23554 $page = '';
23555 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
23556 if ( $this->is_network_registered() ) {
23557 $page = 'account';
23558 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
23559 $this->maybe_set_slug_and_network_menu_exists_flag();
23560 }
23561 }
23562
23563 $url = $this->_get_admin_page_url( $page );
23564 }
23565 } else {
23566 $plugin_fs = false;
23567
23568 if ( $this->is_parent_plugin_installed() ) {
23569 $plugin_fs = self::get_parent_instance();
23570 }
23571
23572 if ( is_object( $plugin_fs ) ) {
23573 if ( ! $plugin_fs->is_registered() ) {
23574 // Forward to parent plugin connect when parent not registered.
23575 $url = $plugin_fs->get_activation_url();
23576 } else {
23577 // Forward to account page.
23578 $url = $plugin_fs->_get_admin_page_url( 'account' );
23579 }
23580 }
23581 }
23582
23583 return $url;
23584 }
23585
23586 /**
23587 * Forward page to activation page.
23588 *
23589 * @author Vova Feldman (@svovaf)
23590 * @since 1.0.3
23591 */
23592 function _redirect_on_activation_hook() {
23593 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
23594 $url = $this->get_after_plugin_activation_redirect_url();
23595
23596 if ( is_string( $url ) ) {
23597 fs_redirect( $url );
23598 }
23599 }
23600 }
23601
23602 /**
23603 * Modify plugin's page action links collection.
23604 *
23605 * @author Vova Feldman (@svovaf)
23606 * @since 1.0.0
23607 *
23608 * @param array $links
23609 * @param $file
23610 *
23611 * @return array
23612 */
23613 function _modify_plugin_action_links_hook( $links, $file ) {
23614 $this->_logger->entrance();
23615
23616 $passed_deactivate = false;
23617 $deactivate_link = '';
23618 $before_deactivate = array();
23619 $after_deactivate = array();
23620 foreach ( $links as $key => $link ) {
23621 if ( 'deactivate' === $key ) {
23622 $deactivate_link = $link;
23623 $passed_deactivate = true;
23624 continue;
23625 }
23626
23627 if ( ! $passed_deactivate ) {
23628 $before_deactivate[ $key ] = $link;
23629 } else {
23630 $after_deactivate[ $key ] = $link;
23631 }
23632 }
23633
23634 ksort( $this->_action_links );
23635
23636 foreach ( $this->_action_links as $new_links ) {
23637 foreach ( $new_links as $link ) {
23638 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
23639 }
23640 }
23641
23642 if ( ! empty( $deactivate_link ) ) {
23643 /**
23644 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
23645 *
23646 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
23647 */
23648 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
23649
23650 // Append deactivation link.
23651 $before_deactivate['deactivate'] = $deactivate_link;
23652 }
23653
23654 return array_merge( $before_deactivate, $after_deactivate );
23655 }
23656
23657 /**
23658 * Adds admin message.
23659 *
23660 * @author Vova Feldman (@svovaf)
23661 * @since 1.0.4
23662 *
23663 * @param string $message
23664 * @param string $title
23665 * @param string $type
23666 */
23667 function add_admin_message( $message, $title = '', $type = 'success' ) {
23668 $this->_admin_notices->add( $message, $title, $type );
23669 }
23670
23671 /**
23672 * Adds sticky admin message.
23673 *
23674 * @author Vova Feldman (@svovaf)
23675 * @since 1.1.0
23676 *
23677 * @param string $message
23678 * @param string $id
23679 * @param string $title
23680 * @param string $type
23681 */
23682 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
23683 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
23684 }
23685
23686 /**
23687 * Check if the paid version of the module is installed.
23688 *
23689 * @author Vova Feldman (@svovaf)
23690 * @since 2.2.0
23691 *
23692 * @return bool
23693 */
23694 private function is_premium_version_installed() {
23695 $premium_plugin_basename = $this->premium_plugin_basename();
23696 $premium_plugin = get_plugins( '/' . dirname( $premium_plugin_basename ) );
23697
23698 return ! empty( $premium_plugin );
23699 }
23700
23701 /**
23702 * Helper function that returns the final steps for the upgrade completion.
23703 *
23704 * If the module is already running the premium code, returns an empty string.
23705 *
23706 * @author Vova Feldman (@svovaf)
23707 * @since 1.2.1
23708 *
23709 * @param string $plan_title
23710 *
23711 * @return string
23712 */
23713 private function get_complete_upgrade_instructions( $plan_title = '' ) {
23714 $this->_logger->entrance();
23715
23716 $activate_license_string = $this->get_license_network_activation_notice();
23717
23718 if ( ! $this->has_premium_version() || $this->is_premium() ) {
23719 return '' . $activate_license_string;
23720 }
23721
23722 if ( empty( $plan_title ) ) {
23723 $plan_title = $this->get_plan_title();
23724 }
23725
23726 if ( $this->is_premium_version_installed() ) {
23727 /**
23728 * If the premium version is already installed, instead of showing the installation instructions,
23729 * tell the current user to activate it.
23730 *
23731 * @author Leo Fajardo (@leorw)
23732 * @since 2.2.1
23733 */
23734 $premium_plugin_basename = $this->premium_plugin_basename();
23735
23736 return sprintf(
23737 /* translators: %1$s: Product title; %2$s: Plan title */
23738 $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' ),
23739 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
23740 $plan_title,
23741 sprintf(
23742 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
23743 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_plugin_basename, 'activate-plugin_' . $premium_plugin_basename ),
23744 esc_html( sprintf(
23745 /* translators: %s: Plan title */
23746 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
23747 $plan_title
23748 ) )
23749 )
23750 );
23751 } else {
23752 // @since 1.2.1.5 The free version is auto deactivated.
23753 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
23754 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
23755 '';
23756
23757 return sprintf(
23758 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
23759 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
23760 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
23761 $this->get_latest_download_link( sprintf(
23762 /* translators: %s: Plan title */
23763 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
23764 $plan_title
23765 ) ),
23766 $deactivation_step,
23767 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
23768 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/upload-wp-' . $this->_module_type . 's' ),
23769 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
23770 );
23771 }
23772 }
23773
23774 /**
23775 * @author Leo Fajardo (@leorw)
23776 * @since 2.1.0
23777 *
23778 * @param string $url
23779 * @param array $request
23780 */
23781 private static function enrich_request_for_debug( &$url, &$request ) {
23782 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
23783 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
23784 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
23785
23786 $request['cookies'] = array(
23787 new WP_Http_Cookie( array(
23788 'name' => 'XDEBUG_SESSION',
23789 'value' => 'PHPSTORM',
23790 ) )
23791 );
23792 }
23793 }
23794
23795 /**
23796 * @author Leo Fajardo (@leorw)
23797 * @since 2.1.0
23798 *
23799 * @param string $url
23800 * @param array $request
23801 * @param int $success_cache_expiration
23802 * @param int $failure_cache_expiration
23803 * @param bool $maybe_enrich_request_for_debug
23804 *
23805 * @return WP_Error|array
23806 */
23807 static function safe_remote_post(
23808 &$url,
23809 $request,
23810 $success_cache_expiration = 0,
23811 $failure_cache_expiration = 0,
23812 $maybe_enrich_request_for_debug = true
23813 ) {
23814 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
23815
23816 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
23817
23818 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
23819 get_transient( $cache_key ) :
23820 false;
23821
23822 if ( false === $response ) {
23823 if ( $maybe_enrich_request_for_debug ) {
23824 self::enrich_request_for_debug( $url, $request );
23825 }
23826
23827 $response = wp_remote_post( $url, $request );
23828
23829 if ( $response instanceof WP_Error ) {
23830 if ( 'https://' === substr( $url, 0, 8 ) &&
23831 isset( $response->errors ) &&
23832 isset( $response->errors['http_request_failed'] )
23833 ) {
23834 $http_error = strtolower( $response->errors['http_request_failed'][0] );
23835
23836 if ( false !== strpos( $http_error, 'ssl' ) ||
23837 false !== strpos( $http_error, 'curl error 35' )
23838 ) {
23839 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
23840 $url = 'http://' . substr( $url, 8 );
23841
23842 $request['timeout'] = 15;
23843 $response = wp_remote_post( $url, $request );
23844 }
23845 }
23846 }
23847
23848 if ( false !== $cache_key ) {
23849 set_transient(
23850 $cache_key,
23851 $response,
23852 ( ( $response instanceof WP_Error ) ?
23853 $failure_cache_expiration :
23854 $success_cache_expiration )
23855 );
23856 }
23857 }
23858
23859 return $response;
23860 }
23861
23862 /**
23863 * This method is used to enrich the after upgrade notice instructions when the upgraded
23864 * license cannot be activated network wide (license quota isn't large enough).
23865 *
23866 * @author Vova Feldman (@svovaf)
23867 * @since 2.0.0
23868 *
23869 * @return string
23870 */
23871 private function get_license_network_activation_notice() {
23872 if ( ! $this->_is_network_active ) {
23873 // Module isn't network level activated.
23874 return '';
23875 }
23876
23877 if ( ! fs_is_network_admin() ) {
23878 // Not network level admin.
23879 return '';
23880 }
23881
23882 if ( get_blog_count() == 1 ) {
23883 // There's only a single site in the network so if there's a context license it was already activated.
23884 return '';
23885 }
23886
23887 if ( ! is_object( $this->_license ) ) {
23888 // No context license.
23889 return '';
23890 }
23891
23892 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
23893 // 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).
23894 return '';
23895 }
23896
23897 if ( $this->can_activate_license_on_network( $this->_license ) ) {
23898 // 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).
23899 return '';
23900 }
23901
23902 return sprintf(
23903 $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' ),
23904 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
23905 '</a>'
23906 );
23907 }
23908
23909 /**
23910 * @author Vova Feldman (@svovaf)
23911 * @since 1.2.1.7
23912 *
23913 * @param string $key
23914 *
23915 * @return string
23916 */
23917 function get_text( $key ) {
23918 return fs_text( $key, $this->_slug );
23919 }
23920
23921 /**
23922 * @author Vova Feldman (@svovaf)
23923 * @since 1.2.3
23924 *
23925 * @param string $text Translatable string.
23926 * @param string $key String key for overrides.
23927 *
23928 * @return string
23929 */
23930 function get_text_inline( $text, $key = '' ) {
23931 return _fs_text_inline( $text, $key, $this->_slug );
23932 }
23933
23934 /**
23935 * @author Vova Feldman (@svovaf)
23936 * @since 1.2.3
23937 *
23938 * @param string $text Translatable string.
23939 * @param string $context Context information for the translators.
23940 * @param string $key String key for overrides.
23941 *
23942 * @return string
23943 */
23944 function get_text_x_inline( $text, $context, $key ) {
23945 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
23946 }
23947
23948 /**
23949 * @author Vova Feldman (@svovaf)
23950 * @since 1.2.3
23951 *
23952 * @param string $text Translatable string.
23953 * @param string $key String key for overrides.
23954 *
23955 * @return string
23956 */
23957 function esc_html_inline( $text, $key ) {
23958 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
23959 }
23960
23961 #----------------------------------------------------------------------------------
23962 #region Versioning
23963 #----------------------------------------------------------------------------------
23964
23965 /**
23966 * Check if Freemius in SDK upgrade mode.
23967 *
23968 * @author Vova Feldman (@svovaf)
23969 * @since 1.0.9
23970 *
23971 * @return bool
23972 */
23973 function is_sdk_upgrade_mode() {
23974 return isset( $this->_storage->sdk_upgrade_mode ) ?
23975 $this->_storage->sdk_upgrade_mode :
23976 false;
23977 }
23978
23979 /**
23980 * Turn SDK upgrade mode off.
23981 *
23982 * @author Vova Feldman (@svovaf)
23983 * @since 1.0.9
23984 */
23985 function set_sdk_upgrade_complete() {
23986 $this->_storage->sdk_upgrade_mode = false;
23987 }
23988
23989 /**
23990 * Check if plugin upgrade mode.
23991 *
23992 * @author Vova Feldman (@svovaf)
23993 * @since 1.0.9
23994 *
23995 * @return bool
23996 */
23997 function is_plugin_upgrade_mode() {
23998 return isset( $this->_storage->plugin_upgrade_mode ) ?
23999 $this->_storage->plugin_upgrade_mode :
24000 false;
24001 }
24002
24003 /**
24004 * Turn plugin upgrade mode off.
24005 *
24006 * @author Vova Feldman (@svovaf)
24007 * @since 1.0.9
24008 */
24009 function set_plugin_upgrade_complete() {
24010 $this->_storage->plugin_upgrade_mode = false;
24011
24012 $license_migration = ! empty( $this->_storage->license_migration ) ?
24013 $this->_storage->license_migration :
24014 array();
24015
24016 $license_migration['is_migrating'] = false;
24017
24018 $this->_storage->license_migration = $license_migration;
24019 }
24020
24021 #endregion
24022
24023 #----------------------------------------------------------------------------------
24024 #region Permissions
24025 #----------------------------------------------------------------------------------
24026
24027 /**
24028 * Check if specific permission requested.
24029 *
24030 * @author Vova Feldman (@svovaf)
24031 * @since 1.1.6
24032 *
24033 * @param string $permission
24034 *
24035 * @return bool
24036 */
24037 function is_permission_requested( $permission ) {
24038 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
24039 }
24040
24041 #endregion
24042
24043 #----------------------------------------------------------------------------------
24044 #region Auto Activation
24045 #----------------------------------------------------------------------------------
24046
24047 /**
24048 * Hints the SDK if running an auto-installation.
24049 *
24050 * @var bool
24051 */
24052 private $_isAutoInstall = false;
24053
24054 /**
24055 * After upgrade callback to install and auto activate a plugin.
24056 * This code will only be executed on explicit request from the user,
24057 * following the practice Jetpack are using with their theme installations.
24058 *
24059 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
24060 *
24061 * @author Vova Feldman (@svovaf)
24062 * @since 1.2.1.7
24063 */
24064 function _install_premium_version_ajax_action() {
24065 $this->_logger->entrance();
24066
24067 $this->check_ajax_referer( 'install_premium_version' );
24068
24069 if ( ! $this->is_registered() ) {
24070 // Not registered.
24071 self::shoot_ajax_failure( array(
24072 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
24073 'code' => 'premium_installed',
24074 ) );
24075 }
24076
24077 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
24078
24079 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24080 // Invalid ID.
24081 self::shoot_ajax_failure( array(
24082 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24083 'code' => 'invalid_module_id',
24084 ) );
24085 }
24086
24087 if ( $plugin_id == $this->get_id() ) {
24088 if ( $this->is_premium() ) {
24089 // Already using the premium code version.
24090 self::shoot_ajax_failure( array(
24091 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
24092 'code' => 'premium_installed',
24093 ) );
24094 }
24095 if ( ! $this->can_use_premium_code() ) {
24096 // Don't have access to the premium code.
24097 self::shoot_ajax_failure( array(
24098 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
24099 'code' => 'invalid_license',
24100 ) );
24101 }
24102 if ( ! $this->has_release_on_freemius() ) {
24103 // Plugin is a serviceware, no premium code version.
24104 self::shoot_ajax_failure( array(
24105 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
24106 'code' => 'premium_version_missing',
24107 ) );
24108 }
24109 } else {
24110 $addon = $this->get_addon( $plugin_id );
24111
24112 if ( ! is_object( $addon ) ) {
24113 // Invalid add-on ID.
24114 self::shoot_ajax_failure( array(
24115 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24116 'code' => 'invalid_module_id',
24117 ) );
24118 }
24119
24120 if ( $this->is_addon_activated( $plugin_id, true ) ) {
24121 // Premium add-on version is already activated.
24122 self::shoot_ajax_failure( array(
24123 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
24124 'code' => 'premium_installed',
24125 ) );
24126 }
24127 }
24128
24129 $this->_isAutoInstall = true;
24130
24131 // Try to install and activate.
24132 $updater = FS_Plugin_Updater::instance( $this );
24133 $result = $updater->install_and_activate_plugin( $plugin_id );
24134
24135 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
24136 self::shoot_ajax_failure( array(
24137 'message' => $result['message'],
24138 'code' => $result['code'],
24139 ) );
24140 }
24141
24142 self::shoot_ajax_success( $result );
24143 }
24144
24145 /**
24146 * Displays module activation dialog box after a successful upgrade
24147 * where the user explicitly requested to auto download and install
24148 * the premium version.
24149 *
24150 * @author Vova Feldman (@svovaf)
24151 * @since 1.2.1.7
24152 */
24153 function _add_auto_installation_dialog_box() {
24154 $this->_logger->entrance();
24155
24156 if ( ! $this->is_registered() ) {
24157 // Not registered.
24158 return;
24159 }
24160
24161 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
24162
24163 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24164 // Invalid module ID.
24165 return;
24166 }
24167
24168 if ( $plugin_id == $this->get_id() ) {
24169 if ( $this->is_premium() ) {
24170 // Already using the premium code version.
24171 return;
24172 }
24173 if ( ! $this->can_use_premium_code() ) {
24174 // Don't have access to the premium code.
24175 return;
24176 }
24177 if ( ! $this->has_release_on_freemius() ) {
24178 // Plugin is a serviceware, no premium code version.
24179 return;
24180 }
24181 } else {
24182 $addon = $this->get_addon( $plugin_id );
24183
24184 if ( ! is_object( $addon ) ) {
24185 // Invalid add-on ID.
24186 return;
24187 }
24188
24189 if ( $this->is_addon_activated( $plugin_id, true ) ) {
24190 // Premium add-on version is already activated.
24191 return;
24192 }
24193 }
24194
24195 $vars = array(
24196 'id' => $this->_module_id,
24197 'target_module_id' => $plugin_id,
24198 'slug' => $this->_slug,
24199 );
24200
24201 fs_require_template( 'auto-installation.php', $vars );
24202 }
24203
24204 #endregion
24205
24206 #--------------------------------------------------------------------------------
24207 #region Tabs Integration
24208 #--------------------------------------------------------------------------------
24209
24210 #region Module's Original Tabs
24211
24212 /**
24213 * Inject a JavaScript logic to capture the theme tabs HTML.
24214 *
24215 * @author Vova Feldman (@svovaf)
24216 * @since 1.2.2.7
24217 */
24218 function _tabs_capture() {
24219 $this->_logger->entrance();
24220
24221 if ( ! $this->is_product_settings_page() ||
24222 ! $this->is_matching_url( $this->main_menu_url() )
24223 ) {
24224 return;
24225 }
24226
24227 $params = array(
24228 'id' => $this->_module_id,
24229 );
24230
24231 fs_require_once_template( 'tabs-capture-js.php', $params );
24232 }
24233
24234 /**
24235 * Cache theme's tabs HTML for a week. The cache will also be set as expired
24236 * after version and type (free/premium) changes, in addition to the week period.
24237 *
24238 * @author Vova Feldman (@svovaf)
24239 * @since 1.2.2.7
24240 */
24241 function _store_tabs_ajax_action() {
24242 $this->_logger->entrance();
24243
24244 $this->check_ajax_referer( 'store_tabs' );
24245
24246 // Init filesystem if not yet initiated.
24247 WP_Filesystem();
24248
24249 // Get POST body HTML data.
24250 global $wp_filesystem;
24251 $tabs_html = $wp_filesystem->get_contents( "php://input" );
24252
24253 if ( is_string( $tabs_html ) ) {
24254 $tabs_html = trim( $tabs_html );
24255 }
24256
24257 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
24258 self::shoot_ajax_failure();
24259 }
24260
24261 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
24262
24263 self::shoot_ajax_success();
24264 }
24265
24266 /**
24267 * Cache theme's settings page custom styles. The cache will also be set as expired
24268 * after version and type (free/premium) changes, in addition to the week period.
24269 *
24270 * @author Vova Feldman (@svovaf)
24271 * @since 1.2.2.7
24272 */
24273 function _store_tabs_styles() {
24274 $this->_logger->entrance();
24275
24276 if ( ! $this->is_product_settings_page() ||
24277 ! $this->is_matching_url( $this->main_menu_url() )
24278 ) {
24279 return;
24280 }
24281
24282 $wp_styles = wp_styles();
24283
24284 $theme_styles_url = get_template_directory_uri();
24285
24286 $stylesheets = array();
24287 foreach ( $wp_styles->queue as $handler ) {
24288 if ( fs_starts_with( $handler, 'fs_' ) ) {
24289 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
24290 continue;
24291 }
24292
24293 /**
24294 * @var _WP_Dependency $stylesheet
24295 */
24296 $stylesheet = $wp_styles->registered[ $handler ];
24297
24298 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
24299 $stylesheets[] = $stylesheet->src;
24300 }
24301 }
24302
24303 if ( ! empty( $stylesheets ) ) {
24304 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
24305 }
24306 }
24307
24308 /**
24309 * Check if module's original settings page has any tabs.
24310 *
24311 * @author Vova Feldman (@svovaf)
24312 * @since 1.2.2.7
24313 *
24314 * @return bool
24315 */
24316 private function has_tabs() {
24317 return $this->_cache->has( 'tabs' );
24318 }
24319
24320 /**
24321 * Get module's settings page HTML content, starting
24322 * from the beginning of the <div class="wrap"> element,
24323 * until the tabs HTML (including).
24324 *
24325 * @author Vova Feldman (@svovaf)
24326 * @since 1.2.2.7
24327 *
24328 * @return string
24329 */
24330 private function get_tabs_html() {
24331 $this->_logger->entrance();
24332
24333 return $this->_cache->get( 'tabs' );
24334 }
24335
24336 /**
24337 * Check if page should include tabs.
24338 *
24339 * @author Vova Feldman (@svovaf)
24340 * @since 1.2.2.7
24341 *
24342 * @return bool
24343 */
24344 private function should_page_include_tabs() {
24345 if ( ! $this->has_settings_menu() ) {
24346 // Don't add tabs if no settings at all.
24347 return false;
24348 }
24349
24350 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
24351 // Only add tabs to themes for now.
24352 return false;
24353 }
24354
24355 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
24356 // Only add tabs to monetizing themes.
24357 return false;
24358 }
24359
24360 if ( ! $this->is_product_settings_page() ) {
24361 // Only add tabs if browsing one of the product's setting pages.
24362 return false;
24363 }
24364
24365 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
24366 // Don't include tabs in the activation page.
24367 return false;
24368 }
24369
24370 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
24371 // Don't add tabs on checkout page, we want to reduce distractions
24372 // as much as possible.
24373 return false;
24374 }
24375
24376 return true;
24377 }
24378
24379 /**
24380 * Add the tabs HTML before the setting's page content and
24381 * enqueue any required stylesheets.
24382 *
24383 * @author Vova Feldman (@svovaf)
24384 * @since 1.2.2.7
24385 *
24386 * @return bool If tabs were included.
24387 */
24388 function _add_tabs_before_content() {
24389 $this->_logger->entrance();
24390
24391 if ( ! $this->should_page_include_tabs() ) {
24392 return false;
24393 }
24394
24395 /**
24396 * Enqueue the original stylesheets that are included in the
24397 * theme settings page. That way, if the theme settings has
24398 * some custom _styled_ content above the tabs UI, this
24399 * will make sure that the styling is preserved.
24400 */
24401 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
24402 if ( is_array( $stylesheets ) ) {
24403 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
24404 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
24405 }
24406 }
24407
24408 // Cut closing </div> tag.
24409 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
24410
24411 return true;
24412 }
24413
24414 /**
24415 * Add the tabs closing HTML after the setting's page content.
24416 *
24417 * @author Vova Feldman (@svovaf)
24418 * @since 1.2.2.7
24419 *
24420 * @return bool If tabs closing HTML was included.
24421 */
24422 function _add_tabs_after_content() {
24423 $this->_logger->entrance();
24424
24425 if ( ! $this->should_page_include_tabs() ) {
24426 return false;
24427 }
24428
24429 echo '</div>';
24430
24431 return true;
24432 }
24433
24434 #endregion
24435
24436 /**
24437 * Add in-page JavaScript to inject the Freemius tabs into
24438 * the module's setting tabs section.
24439 *
24440 * @author Vova Feldman (@svovaf)
24441 * @since 1.2.2.7
24442 */
24443 function _add_freemius_tabs() {
24444 $this->_logger->entrance();
24445
24446 if ( ! $this->should_page_include_tabs() ) {
24447 return;
24448 }
24449
24450 $params = array( 'id' => $this->_module_id );
24451 fs_require_once_template( 'tabs.php', $params );
24452 }
24453
24454 #endregion
24455
24456 #--------------------------------------------------------------------------------
24457 #region Customizer Integration for Themes
24458 #--------------------------------------------------------------------------------
24459
24460 /**
24461 * @author Vova Feldman (@svovaf)
24462 * @since 1.2.2.7
24463 *
24464 * @param WP_Customize_Manager $customizer
24465 */
24466 function _customizer_register( $customizer ) {
24467 $this->_logger->entrance();
24468
24469 if ( $this->is_pricing_page_visible() ) {
24470 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
24471
24472 $customizer->add_section( 'freemius_upsell', array(
24473 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
24474 'priority' => 1,
24475 ) );
24476 $customizer->add_setting( 'freemius_upsell', array(
24477 'sanitize_callback' => 'esc_html',
24478 ) );
24479
24480 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
24481 'fs' => $this,
24482 'section' => 'freemius_upsell',
24483 'priority' => 100,
24484 ) ) );
24485 }
24486
24487 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
24488 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
24489
24490 // Main Documentation Link In Customizer Root.
24491 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
24492 'fs' => $this,
24493 'priority' => 1000,
24494 ) ) );
24495 }
24496 }
24497
24498 #endregion
24499
24500 /**
24501 * If the theme has a paid version, add some custom
24502 * styling to the theme's premium version (if exists)
24503 * to highlight that it's the premium version of the
24504 * same theme, making it easier for identification
24505 * after the user upgrades and upload it to the site.
24506 *
24507 * @author Vova Feldman (@svovaf)
24508 * @since 1.2.2.7
24509 */
24510 function _style_premium_theme() {
24511 $this->_logger->entrance();
24512
24513 if ( ! self::is_themes_page() ) {
24514 // Only include in the themes page.
24515 return;
24516 }
24517
24518 if ( ! $this->has_paid_plan() ) {
24519 // Only include if has any paid plans.
24520 return;
24521 }
24522
24523 $params = null;
24524 fs_require_once_template( '/js/jquery.content-change.php', $params );
24525
24526 $params = array(
24527 'slug' => $this->_slug,
24528 'id' => $this->_module_id,
24529 );
24530
24531 fs_require_template( '/js/style-premium-theme.php', $params );
24532 }
24533
24534 /**
24535 * This method will return the absolute URL of the module's local icon.
24536 *
24537 * When you are running your plugin or theme on a **localhost** environment, if the icon
24538 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
24539 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
24540 * If an icon is found, this method will automatically attempt to download the icon and store it
24541 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
24542 *
24543 * It's important to mention that this method is NOT phoning home since the developer will deploy
24544 * the product with the local icon in the assets folder. The download process just simplifies
24545 * the process for the developer.
24546 *
24547 * @author Vova Feldman (@svovaf)
24548 * @since 2.0.0
24549 *
24550 * @return string
24551 */
24552 function get_local_icon_url() {
24553 global $fs_active_plugins;
24554
24555 /**
24556 * @since 1.1.7.5
24557 */
24558 $local_path = $this->apply_filters( 'plugin_icon', false );
24559
24560 if ( is_string( $local_path ) ) {
24561 $icons = array( $local_path );
24562 } else {
24563 $img_dir = WP_FS__DIR_IMG;
24564
24565 // Locate the main assets folder.
24566 if ( 1 < count( $fs_active_plugins->plugins ) ) {
24567 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
24568
24569 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
24570 if ( $data->plugin_path == $this->get_plugin_basename() ) {
24571 $img_dir = $plugin_or_theme_img_dir
24572 . '/'
24573 /**
24574 * The basename will be `themes` or the basename of a custom themes directory.
24575 *
24576 * @author Leo Fajardo (@leorw)
24577 * @since 2.2.3
24578 */
24579 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
24580 . '/assets/img';
24581
24582 break;
24583 }
24584 }
24585 }
24586
24587 // Try to locate the icon in the assets folder.
24588 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
24589
24590 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
24591 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
24592 $icons = array(
24593 fs_normalize_path( $img_dir . '/theme-icon.png' )
24594 );
24595 } else {
24596 $icon_found = false;
24597 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
24598
24599 if ( ! function_exists( 'get_filesystem_method' ) ) {
24600 require_once ABSPATH . 'wp-admin/includes/file.php';
24601 }
24602
24603 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
24604
24605 /**
24606 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
24607 *
24608 * This code will only be executed once during the testing
24609 * of the plugin in a local environment. The plugin icon file WILL
24610 * already exist in the assets folder when the plugin is deployed to
24611 * the repository.
24612 */
24613 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
24614 // Fetch icon from Freemius.
24615 $icon = $this->fetch_remote_icon_url();
24616
24617 // Fetch icon from WordPress.org.
24618 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
24619 if ( ! function_exists( 'plugins_api' ) ) {
24620 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
24621 }
24622
24623 $plugin_information = plugins_api( 'plugin_information', array(
24624 'slug' => $this->_slug,
24625 'fields' => array(
24626 'sections' => false,
24627 'tags' => false,
24628 'icons' => true
24629 )
24630 ) );
24631
24632 if (
24633 ! is_wp_error( $plugin_information )
24634 && isset( $plugin_information->icons )
24635 && ! empty( $plugin_information->icons )
24636 ) {
24637 /**
24638 * Get the smallest icon.
24639 *
24640 * @author Leo Fajardo (@leorw)
24641 * @since 1.2.2
24642 */
24643 $icon = end( $plugin_information->icons );
24644 }
24645 }
24646
24647 if ( ! empty( $icon ) ) {
24648 if ( 0 !== strpos( $icon, 'http' ) ) {
24649 $icon = 'http:' . $icon;
24650 }
24651
24652 /**
24653 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
24654 *
24655 * @author Leo Fajardo (@leorw)
24656 * @since 1.2.2
24657 */
24658 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
24659
24660 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
24661
24662 // Try to download the icon.
24663 $icon_found = fs_download_image( $icon, $local_path );
24664 }
24665 }
24666
24667 if ( ! $icon_found ) {
24668 // No icons found, fallback to default icon.
24669 if ( $have_write_permissions ) {
24670 // If have write permissions, copy default icon.
24671 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
24672 } else {
24673 // If doesn't have write permissions, use default icon path.
24674 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
24675 }
24676 }
24677
24678 $icons = array( $local_path );
24679 }
24680 }
24681 }
24682
24683 $icon_dir = dirname( $icons[0] );
24684
24685 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
24686 }
24687
24688 /**
24689 * Fetch module's extended info.
24690 *
24691 * @author Vova Feldman (@svovaf)
24692 * @since 2.0.0
24693 *
24694 * @return object|mixed
24695 */
24696 private function fetch_module_info() {
24697 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
24698 }
24699
24700 /**
24701 * Fetch module's remote icon URL.
24702 *
24703 * @author Vova Feldman (@svovaf)
24704 * @since 2.0.0
24705 *
24706 * @return string
24707 */
24708 function fetch_remote_icon_url() {
24709 $info = $this->fetch_module_info();
24710
24711 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
24712 $info->icon :
24713 '';
24714 }
24715
24716 #--------------------------------------------------------------------------------
24717 #region GDPR
24718 #--------------------------------------------------------------------------------
24719
24720 /**
24721 * @author Leo Fajardo (@leorw)
24722 * @since 2.1.0
24723 *
24724 * @return bool
24725 */
24726 function fetch_and_store_current_user_gdpr_anonymously() {
24727 $pong = $this->ping( null, true );
24728
24729 if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
24730 return false;
24731 } else {
24732 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
24733
24734 return $pong->is_gdpr_required;
24735 }
24736 }
24737
24738 /**
24739 * @author Leo Fajardo (@leorw)
24740 * @since 2.1.0
24741 *
24742 * @param array $user_plugins
24743 *
24744 * @return string
24745 */
24746 private function get_gdpr_admin_notice_string( $user_plugins ) {
24747 $this->_logger->entrance();
24748
24749 $addons = self::get_all_addons();
24750
24751 foreach ( $user_plugins as $user_plugin ) {
24752 $has_addons = isset( $addons[ $user_plugin->id ] );
24753
24754 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
24755 if ( $this->_module_id == $user_plugin->id ) {
24756 $addons = $this->get_addons();
24757 $has_addons = ( ! empty( $addons ) );
24758 } else {
24759 $plugin_api = FS_Api::instance(
24760 $user_plugin->id,
24761 'plugin',
24762 $user_plugin->id,
24763 $user_plugin->public_key,
24764 ! $user_plugin->is_live,
24765 false,
24766 $this->get_sdk_version()
24767 );
24768
24769 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
24770
24771 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
24772 is_array( $addons_result->plugins ) &&
24773 ! empty( $addons_result->plugins )
24774 ) {
24775 $has_addons = true;
24776 }
24777 }
24778 }
24779
24780 $user_plugin->has_addons = $has_addons;
24781 }
24782
24783 $is_single_parent_product = ( 1 === count( $user_plugins ) );
24784
24785 $multiple_products_text = '';
24786
24787 if ( $is_single_parent_product ) {
24788 $single_parent_product = reset( $user_plugins );
24789
24790 $thank_you = sprintf(
24791 "<span data-plugin-id='%d'>%s</span>",
24792 $single_parent_product->id,
24793 sprintf(
24794 $single_parent_product->has_addons ?
24795 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
24796 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
24797 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
24798 )
24799 );
24800
24801 $already_opted_in = sprintf(
24802 $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' ),
24803 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
24804 );
24805 } else {
24806 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
24807 $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' );
24808
24809 $products_and_add_ons = '';
24810 foreach ( $user_plugins as $user_plugin ) {
24811 if ( ! empty( $products_and_add_ons ) ) {
24812 $products_and_add_ons .= ', ';
24813 }
24814
24815 if ( ! $user_plugin->has_addons ) {
24816 $products_and_add_ons .= sprintf(
24817 "<span data-plugin-id='%d'>%s</span>",
24818 $user_plugin->id,
24819 $user_plugin->title
24820 );
24821 } else {
24822 $products_and_add_ons .= sprintf(
24823 "<span data-plugin-id='%d'>%s</span>",
24824 $user_plugin->id,
24825 sprintf(
24826 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
24827 $user_plugin->title
24828 )
24829 );
24830 }
24831 }
24832
24833 $multiple_products_text = sprintf(
24834 "<small class='products'><strong>%s:</strong> %s</small>",
24835 $this->get_text_inline( 'Products', 'products' ),
24836 $products_and_add_ons
24837 );
24838 }
24839
24840 $actions = sprintf(
24841 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
24842 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
24843 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
24844 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
24845 sprintf(
24846 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
24847 '<span class="underlined">',
24848 '</span>'
24849 )
24850 );
24851
24852 return sprintf(
24853 '%s %s %s',
24854 $thank_you,
24855 $already_opted_in,
24856 sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '<a href="https://eugdpr.org/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
24857 '<br><br>' .
24858 '<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>' .
24859 $actions .
24860 ( $is_single_parent_product ? '' : $multiple_products_text )
24861 );
24862 }
24863
24864 /**
24865 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
24866 * plugins and themes they've opted in to.
24867 *
24868 * @author Leo Fajardo (@leorw)
24869 * @since 2.1.0
24870 *
24871 * @param string $user_email
24872 * @param string $license_key
24873 * @param array $plugin_ids
24874 * @param string|null $license_key
24875 *
24876 * @return array|false
24877 */
24878 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
24879 $request = array(
24880 'method' => 'POST',
24881 'body' => array(),
24882 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
24883 );
24884
24885 if ( is_string( $user_email ) ) {
24886 $request['body']['email'] = $user_email;
24887 } else {
24888 $request['body']['license_key'] = $license_key;
24889 }
24890
24891 $result = array();
24892
24893 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
24894 $total_plugin_ids = count( $plugin_ids );
24895
24896 $plugin_ids_count_per_request = 10;
24897 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
24898 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
24899
24900 $request['body']['plugin_ids'] = $plugin_ids_set;
24901
24902 $response = self::safe_remote_post(
24903 $url,
24904 $request,
24905 WP_FS__TIME_24_HOURS_IN_SEC,
24906 WP_FS__TIME_12_HOURS_IN_SEC
24907 );
24908
24909 if ( ! is_wp_error( $response ) ) {
24910 $decoded = is_string( $response['body'] ) ?
24911 json_decode( $response['body'] ) :
24912 null;
24913
24914 if (
24915 !is_object($decoded) ||
24916 !isset($decoded->success) ||
24917 true !== $decoded->success ||
24918 !isset( $decoded->data ) ||
24919 !is_array( $decoded->data )
24920 ) {
24921 return false;
24922 }
24923
24924 $result = array_merge( $result, $decoded->data );
24925 }
24926 }
24927
24928 return $result;
24929 }
24930
24931 /**
24932 * @author Leo Fajardo (@leorw)
24933 * @since 2.1.0
24934 */
24935 function _maybe_show_gdpr_admin_notice() {
24936 if ( ! $this->is_user_in_admin() ) {
24937 return;
24938 }
24939
24940 if ( ! $this->should_handle_gdpr_admin_notice() ) {
24941 return;
24942 }
24943
24944 if ( ! $this->is_user_admin() ) {
24945 return;
24946 }
24947
24948 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
24949
24950 $lock = FS_User_Lock::instance();
24951
24952 /**
24953 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
24954 */
24955 if ( ! $lock->try_lock( 60 ) ) {
24956 return;
24957 }
24958
24959 /**
24960 * @var $current_wp_user WP_User
24961 */
24962 $current_wp_user = self::_get_current_wp_user();
24963
24964 /**
24965 * @var FS_User $current_fs_user
24966 */
24967 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
24968
24969 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
24970
24971 if ( ! is_object( $current_fs_user ) ) {
24972 // 10-year lock.
24973 $lock->lock( $ten_years_in_sec );
24974
24975 return;
24976 }
24977
24978 $gdpr = FS_GDPR_Manager::instance();
24979
24980 if ( $gdpr->is_opt_in_notice_shown() ) {
24981 // 30-day lock.
24982 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
24983
24984 return;
24985 }
24986
24987 if ( ! $gdpr->should_show_opt_in_notice() ) {
24988 // 10-year lock.
24989 $lock->lock( $ten_years_in_sec );
24990
24991 return;
24992 }
24993
24994 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
24995 $was_notice_shown_before = ( false !== $last_time_notice_shown );
24996
24997 if ( $was_notice_shown_before &&
24998 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
24999 ) {
25000 // If the notice was shown before, show it again after 30 days from the last time it was shown.
25001 return;
25002 }
25003
25004 /**
25005 * Find all plugin IDs that were installed by the current admin.
25006 */
25007 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
25008
25009 if ( empty( $plugin_ids_map )) {
25010 $lock->lock( $ten_years_in_sec );
25011
25012 return;
25013 }
25014
25015 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25016 $current_fs_user->email,
25017 null,
25018 array_keys( $plugin_ids_map )
25019 );
25020
25021 if ( empty( $user_plugins ) ) {
25022 $lock->lock(
25023 is_array($user_plugins) ?
25024 $ten_years_in_sec :
25025 // Lock for 24-hours on errors.
25026 WP_FS__TIME_24_HOURS_IN_SEC
25027 );
25028
25029 return;
25030 }
25031
25032 $has_unset_marketing_optin = false;
25033
25034 foreach ( $user_plugins as $user_plugin ) {
25035 if ( true == $user_plugin->is_marketing_allowed ) {
25036 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
25037 }
25038
25039 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
25040 $has_unset_marketing_optin = true;
25041 }
25042 }
25043
25044 if ( empty( $plugin_ids_map ) ||
25045 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
25046 ) {
25047 $lock->lock( $ten_years_in_sec );
25048
25049 return;
25050 }
25051
25052 $modules = array_merge(
25053 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25054 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25055 );
25056
25057 foreach ( $modules as $module ) {
25058 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
25059 $plugin_ids_map[ $module->id ] = $module;
25060 }
25061 }
25062
25063 $plugin_title = null;
25064 if ( 1 === count( $plugin_ids_map ) ) {
25065 $module = reset( $plugin_ids_map );
25066 $plugin_title = $module->title;
25067 }
25068
25069 $gdpr->add_opt_in_sticky_notice(
25070 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
25071 $plugin_title
25072 );
25073
25074 $this->add_gdpr_optin_ajax_handler_and_style();
25075
25076 $gdpr->notice_was_just_shown();
25077
25078 // 30-day lock.
25079 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25080 }
25081
25082 /**
25083 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
25084 * marketing.
25085 *
25086 * @author Leo Fajardo (@leorw)
25087 * @since 2.1.0
25088 */
25089 private function disable_opt_in_notice_and_lock_user() {
25090 FS_GDPR_Manager::instance()->disable_opt_in_notice();
25091
25092 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25093
25094 // 10-year lock.
25095 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
25096 }
25097
25098 /**
25099 * @author Leo Fajardo (@leorw)
25100 * @since 2.1.0
25101 */
25102 function _add_gdpr_optin_js() {
25103 $vars = array( 'id' => $this->_module_id );
25104
25105 fs_require_once_template( 'gdpr-optin-js.php', $vars );
25106 }
25107
25108 /**
25109 * @author Leo Fajardo (@leorw)
25110 * @since 2.1.0
25111 */
25112 function enqueue_gdpr_optin_notice_style() {
25113 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
25114 }
25115
25116 /**
25117 * @author Leo Fajardo (@leorw)
25118 * @since 2.1.0
25119 */
25120 function _maybe_add_gdpr_optin_ajax_handler() {
25121 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
25122
25123 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
25124 $this->add_gdpr_optin_ajax_handler_and_style();
25125 }
25126 }
25127
25128 /**
25129 * @author Leo Fajardo (@leorw)
25130 * @since 2.1.0
25131 */
25132 function _fetch_is_marketing_required_flag_value_ajax_action() {
25133 $this->_logger->entrance();
25134
25135 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
25136
25137 $license_key = fs_request_get( 'license_key' );
25138
25139 if ( empty($license_key) ) {
25140 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
25141 }
25142
25143 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25144 null,
25145 $license_key,
25146 array( $this->_module_id )
25147 );
25148
25149 if ( ! is_array( $user_plugins ) ||
25150 empty($user_plugins) ||
25151 !isset($user_plugins[0]->plugin_id) ||
25152 $user_plugins[0]->plugin_id != $this->_module_id
25153 ) {
25154 /**
25155 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
25156 *
25157 * @author Vova Feldman (@svovaf)
25158 */
25159 self::shoot_ajax_success( array(
25160 'is_marketing_allowed' => null,
25161 'license_owner_id' => null
25162 ) );
25163 }
25164
25165 self::shoot_ajax_success( array(
25166 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
25167 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
25168 ) );
25169 }
25170
25171 /**
25172 * @author Leo Fajardo (@leorw)
25173 * @since 2.3.2
25174 *
25175 * @param number[] $install_ids
25176 *
25177 * @return array {
25178 * An array of objects containing the installs' licenses owners data.
25179 *
25180 * @property number $id User ID.
25181 * @property string $email User email (can be masked email).
25182 * }
25183 */
25184 private function fetch_installs_licenses_owners_data( $install_ids ) {
25185 $this->_logger->entrance();
25186
25187 $response = $this->get_api_user_scope()->get(
25188 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
25189 );
25190
25191 $license_owners = null;
25192
25193 if ( $this->is_api_result_object( $response, 'owners' ) ) {
25194 $license_owners = $response->owners;
25195 }
25196
25197 return $license_owners;
25198 }
25199
25200 /**
25201 * @author Leo Fajardo (@leorw)
25202 * @since 2.1.0
25203 */
25204 private function add_gdpr_optin_ajax_handler_and_style() {
25205 // Add GDPR action AJAX callback.
25206 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
25207
25208 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
25209 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
25210 }
25211
25212 /**
25213 * @author Leo Fajardo (@leorw)
25214 * @since 2.1.0
25215 */
25216 function _gdpr_optin_ajax_action() {
25217 $this->_logger->entrance();
25218
25219 $this->check_ajax_referer( 'gdpr_optin_action' );
25220
25221 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
25222 self::shoot_ajax_failure();
25223 }
25224
25225 $current_wp_user = self::_get_current_wp_user();
25226
25227 $plugin_ids = fs_request_get( 'plugin_ids', array() );
25228 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
25229 self::shoot_ajax_failure();
25230 }
25231
25232 $modules = array_merge(
25233 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25234 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25235 );
25236
25237 foreach ( $modules as $key => $module ) {
25238 if ( ! in_array( $module->id, $plugin_ids ) ) {
25239 unset( $modules[ $key ] );
25240 }
25241 }
25242
25243 if ( empty( $modules ) ) {
25244 self::shoot_ajax_failure();
25245 }
25246
25247 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
25248
25249 foreach ( $modules as $module ) {
25250 $user_api->call( "?plugin_id={$module->id}", 'put', array(
25251 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
25252 ) );
25253 }
25254
25255 FS_GDPR_Manager::instance()->remove_opt_in_notice();
25256
25257 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25258
25259 // 10-year lock.
25260 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
25261
25262 self::shoot_ajax_success();
25263 }
25264
25265 /**
25266 * 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.
25267 *
25268 * @author Vova Feldman (@svovaf)
25269 * @since 2.1.0
25270 *
25271 * @return bool
25272 */
25273 private function should_handle_gdpr_admin_notice() {
25274 return $this->apply_filters(
25275 'handle_gdpr_admin_notice',
25276 // Default to false.
25277 false
25278 );
25279 }
25280
25281 #endregion
25282
25283 #----------------------------------------------------------------------------------
25284 #region Marketing
25285 #----------------------------------------------------------------------------------
25286
25287 /**
25288 * Check if current user purchased any other plugins before.
25289 *
25290 * @author Vova Feldman (@svovaf)
25291 * @since 1.0.9
25292 *
25293 * @return bool
25294 */
25295 function has_purchased_before() {
25296 // TODO: Implement has_purchased_before() method.
25297 throw new Exception( 'not implemented' );
25298 }
25299
25300 /**
25301 * Check if current user classified as an agency.
25302 *
25303 * @author Vova Feldman (@svovaf)
25304 * @since 1.0.9
25305 *
25306 * @return bool
25307 */
25308 function is_agency() {
25309 // TODO: Implement is_agency() method.
25310 throw new Exception( 'not implemented' );
25311 }
25312
25313 /**
25314 * Check if current user classified as a developer.
25315 *
25316 * @author Vova Feldman (@svovaf)
25317 * @since 1.0.9
25318 *
25319 * @return bool
25320 */
25321 function is_developer() {
25322 // TODO: Implement is_developer() method.
25323 throw new Exception( 'not implemented' );
25324 }
25325
25326 /**
25327 * Check if current user classified as a business.
25328 *
25329 * @author Vova Feldman (@svovaf)
25330 * @since 1.0.9
25331 *
25332 * @return bool
25333 */
25334 function is_business() {
25335 // TODO: Implement is_business() method.
25336 throw new Exception( 'not implemented' );
25337 }
25338
25339 #endregion
25340
25341 #----------------------------------------------------------------------------------
25342 #region Helper
25343 #----------------------------------------------------------------------------------
25344
25345 /**
25346 * If running with a secret key, assume it's the developer and show pending plans as well.
25347 *
25348 * @author Vova Feldman (@svovaf)
25349 * @since 2.1.2
25350 *
25351 * @param string $path
25352 *
25353 * @return string
25354 */
25355 function add_show_pending( $path ) {
25356 if ( ! $this->has_secret_key() ) {
25357 return $path;
25358 }
25359
25360 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
25361 }
25362
25363 #endregion
25364 }
25365