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

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

25,388 lines 932.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 1.1.7.5
114 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
115 */
116 private $_anonymous_mode;
117
118 /**
119 * @since 1.1.9
120 * @var bool Hints the SDK if plugin have any free plans.
121 */
122 private $_is_premium_only;
123
124 /**
125 * @since 1.2.1.6
126 * @var bool Hints the SDK if plugin have premium code version at all.
127 */
128 private $_has_premium_version;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
133 */
134 private $_ignore_pending_mode;
135
136 /**
137 * @since 1.0.8
138 * @var bool Hints the SDK if the plugin has any paid plans.
139 */
140 private $_has_paid_plans;
141
142 /**
143 * @since 1.2.1.5
144 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
145 * without a specified period, if positive - the number of trial days.
146 */
147 private $_trial_days = - 1;
148
149 /**
150 * @since 1.2.1.5
151 * @var bool Hints the SDK if the trial requires a payment method or not.
152 */
153 private $_is_trial_require_payment = false;
154
155 /**
156 * @since 1.0.7
157 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
158 */
159 private $_is_org_compliant;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is has add-ons.
164 */
165 private $_has_addons;
166
167 /**
168 * @since 2.4.5
169 * @var string Navigation type: 'menu' or 'tabs'.
170 */
171 private $_navigation;
172
173 const NAVIGATION_MENU = 'menu';
174 const NAVIGATION_TABS = 'tabs';
175
176 /**
177 * @since 1.1.6
178 * @var string[]bool.
179 */
180 private $_permissions;
181
182 /**
183 * @var FS_Storage
184 */
185 private $_storage;
186
187 /**
188 * @since 1.2.2.7
189 * @var FS_Cache_Manager
190 */
191 private $_cache;
192
193 /**
194 * @since 1.0.0
195 *
196 * @var FS_Logger
197 */
198 private $_logger;
199 /**
200 * @since 1.0.4
201 *
202 * @var FS_Plugin
203 */
204 private $_plugin = false;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin|false
209 */
210 private $_parent_plugin = false;
211 /**
212 * @since 1.1.1
213 *
214 * @var Freemius
215 */
216 private $_parent = false;
217 /**
218 * @since 1.0.1
219 *
220 * @var FS_User
221 */
222 private $_user = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_Site
227 */
228 private $_site = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Plugin_License
233 */
234 private $_license;
235 /**
236 * @since 1.0.2
237 *
238 * @var FS_Plugin_Plan[]
239 */
240 private $_plans = false;
241 /**
242 * @var FS_Plugin_License[]
243 * @since 1.0.5
244 */
245 private $_licenses = false;
246
247 /**
248 * @since 1.0.1
249 *
250 * @var FS_Admin_Menu_Manager
251 */
252 private $_menu;
253
254 /**
255 * @var FS_Admin_Notices
256 */
257 private $_admin_notices;
258
259 /**
260 * @since 1.1.6
261 *
262 * @var FS_Admin_Notices
263 */
264 private static $_global_admin_notices;
265
266 /**
267 * @var FS_Logger
268 * @since 1.0.0
269 */
270 private static $_static_logger;
271
272 /**
273 * @var FS_Options
274 * @since 1.0.2
275 */
276 private static $_accounts;
277
278 /**
279 * @since 1.2.2
280 *
281 * @var number
282 */
283 private $_module_id;
284
285 /**
286 * @var Freemius[]
287 */
288 private static $_instances = array();
289
290 /**
291 * @since 1.2.3
292 *
293 * @var FS_Affiliate
294 */
295 private $affiliate = null;
296
297 /**
298 * @since 1.2.3
299 *
300 * @var FS_AffiliateTerms
301 */
302 private $plugin_affiliate_terms = null;
303
304 /**
305 * @since 1.2.3
306 *
307 * @var FS_AffiliateTerms
308 */
309 private $custom_affiliate_terms = null;
310
311 /**
312 * @since 2.0.0
313 *
314 * @var bool
315 */
316 private $_is_multisite_integrated;
317
318 /**
319 * @since 2.0.0
320 *
321 * @var bool True if the current request is for a network admin screen and the plugin is network active.
322 */
323 private $_is_network_active;
324
325 /**
326 * @since 2.0.0
327 *
328 * @var int|null The original blog ID the plugin was loaded with.
329 */
330 private $_blog_id = null;
331
332 /**
333 * @since 2.0.0
334 *
335 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
336 */
337 private $_context_is_network_or_blog_id = null;
338
339 /**
340 * @since 2.0.0
341 *
342 * @var string
343 */
344 private $_dynamically_added_top_level_page_hook_name = '';
345
346 /**
347 * @author Leo Fajardo (@leorw)
348 * @since 2.3.1
349 *
350 * @var bool
351 */
352 private $is_whitelabeled;
353
354 /**
355 * @author Leo Fajardo (@leorw)
356 * @since 2.4.0
357 *
358 * @var bool
359 */
360 private $_is_bundle_license_auto_activation_enabled = false;
361
362 #region Uninstall Reasons IDs
363
364 const REASON_NO_LONGER_NEEDED = 1;
365 const REASON_FOUND_A_BETTER_PLUGIN = 2;
366 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
367 const REASON_BROKE_MY_SITE = 4;
368 const REASON_SUDDENLY_STOPPED_WORKING = 5;
369 const REASON_CANT_PAY_ANYMORE = 6;
370 const REASON_OTHER = 7;
371 const REASON_DIDNT_WORK = 8;
372 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
373 const REASON_COULDNT_MAKE_IT_WORK = 10;
374 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
375 const REASON_NOT_WORKING = 12;
376 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
377 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
378 const REASON_TEMPORARY_DEACTIVATION = 15;
379
380 /**
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 check_admin_referer( 'fs_toggle_debug_mode' );
3554
3555 if ( ! is_super_admin() ) {
3556 return;
3557 }
3558
3559 $is_on = fs_request_get( 'is_on', false, 'post' );
3560
3561 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3562 update_option( 'fs_debug_mode', $is_on );
3563
3564 // Turn on/off storage logging.
3565 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3566 }
3567
3568 exit;
3569 }
3570
3571 /**
3572 * @author Vova Feldman (@svovaf)
3573 * @since 1.2.1.6
3574 */
3575 static function _get_debug_log() {
3576 check_admin_referer( 'fs_get_debug_log' );
3577
3578 if ( ! is_super_admin() ) {
3579 return;
3580 }
3581
3582 $limit = min( ! empty( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 200, 200 );
3583 $offset = min( ! empty( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 200, 200 );
3584
3585 $logs = FS_Logger::load_db_logs(
3586 fs_request_get( 'filters', false, 'post' ),
3587 $limit,
3588 $offset
3589 );
3590
3591 self::shoot_ajax_success( $logs );
3592 }
3593
3594 /**
3595 * @author Vova Feldman (@svovaf)
3596 * @since 1.2.1.7
3597 */
3598 static function _get_db_option() {
3599 check_admin_referer( 'fs_get_db_option' );
3600
3601 $option_name = fs_request_get( 'option_name' );
3602
3603 if ( ! is_super_admin() ||
3604 ! fs_starts_with( $option_name, 'fs_' )
3605 ) {
3606 self::shoot_ajax_failure();
3607 }
3608
3609 $value = get_option( $option_name );
3610
3611 $result = array(
3612 'name' => $option_name,
3613 );
3614
3615 if ( false !== $value ) {
3616 if ( ! is_string( $value ) ) {
3617 $value = json_encode( $value );
3618 }
3619
3620 $result['value'] = $value;
3621 }
3622
3623 self::shoot_ajax_success( $result );
3624 }
3625
3626 /**
3627 * @author Vova Feldman (@svovaf)
3628 * @since 1.2.1.7
3629 */
3630 static function _set_db_option() {
3631 check_admin_referer( 'fs_set_db_option' );
3632
3633 $option_name = fs_request_get( 'option_name' );
3634
3635 if ( ! is_super_admin() ||
3636 ! fs_starts_with( $option_name, 'fs_' )
3637 ) {
3638 self::shoot_ajax_failure();
3639 }
3640
3641 $option_value = fs_request_get( 'option_value' );
3642
3643 if ( ! empty( $option_value ) ) {
3644 update_option( $option_name, $option_value );
3645 }
3646
3647 self::shoot_ajax_success();
3648 }
3649
3650 /**
3651 * @author Vova Feldman (@svovaf)
3652 * @since 1.0.8
3653 */
3654 static function _debug_page_actions() {
3655 self::_clean_admin_content_section();
3656
3657 if ( fs_request_is_action( 'restart_freemius' ) ) {
3658 check_admin_referer( 'restart_freemius' );
3659
3660 if ( ! is_multisite() ) {
3661 // Clear accounts data.
3662 self::$_accounts->clear( null, true );
3663 } else {
3664 $sites = self::get_sites();
3665 foreach ( $sites as $site ) {
3666 $blog_id = self::get_site_blog_id( $site );
3667 self::$_accounts->clear( $blog_id, true );
3668 }
3669
3670 // Clear network level storage.
3671 self::$_accounts->clear( true, true );
3672 }
3673
3674 // Clear SDK reference cache.
3675 delete_option( 'fs_active_plugins' );
3676 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3677 check_admin_referer( 'clear_updates_data' );
3678
3679 if ( ! is_multisite() ) {
3680 set_site_transient( 'update_plugins', null );
3681 set_site_transient( 'update_themes', null );
3682 } else {
3683 $current_blog_id = get_current_blog_id();
3684
3685 $sites = self::get_sites();
3686 foreach ( $sites as $site ) {
3687 switch_to_blog( self::get_site_blog_id( $site ) );
3688
3689 set_site_transient( 'update_plugins', null );
3690 set_site_transient( 'update_themes', null );
3691 }
3692
3693 switch_to_blog( $current_blog_id );
3694 }
3695 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3696 check_admin_referer( 'simulate_trial' );
3697
3698 $fs = freemius( fs_request_get( 'module_id' ) );
3699
3700 // Update SDK install to at least 24 hours before.
3701 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3702 // Unset the trial shown timestamp.
3703 unset( $fs->_storage->trial_promotion_shown );
3704 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3705 check_admin_referer( 'simulate_network_upgrade' );
3706
3707 $fs = freemius( fs_request_get( 'module_id' ) );
3708
3709 self::set_network_upgrade_mode( $fs->_storage );
3710 } else if ( fs_request_is_action( 'delete_install' ) ) {
3711 check_admin_referer( 'delete_install' );
3712
3713 self::_delete_site_by_slug(
3714 fs_request_get( 'slug' ),
3715 fs_request_get( 'module_type' ),
3716 true,
3717 fs_request_get( 'blog_id', null )
3718 );
3719 } else if ( fs_request_is_action( 'delete_user' ) ) {
3720 check_admin_referer( 'delete_user' );
3721
3722 self::delete_user( fs_request_get( 'user_id' ) );
3723 } else if ( fs_request_is_action( 'download_logs' ) ) {
3724 check_admin_referer( 'download_logs' );
3725
3726 $download_url = FS_Logger::download_db_logs(
3727 fs_request_get( 'filters', false, 'post' )
3728 );
3729
3730 if ( false === $download_url ) {
3731 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.' );
3732 }
3733
3734 fs_redirect( $download_url );
3735 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3736 check_admin_referer( 'migrate_options_to_network' );
3737
3738 self::migrate_options_to_network();
3739 }
3740 }
3741
3742 /**
3743 * @author Vova Feldman (@svovaf)
3744 * @since 1.0.8
3745 */
3746 static function _debug_page_render() {
3747 self::$_static_logger->entrance();
3748
3749 if ( ! is_multisite() ) {
3750 $all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
3751 $all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME );
3752 } else {
3753 $sites = self::get_sites();
3754
3755 $all_plugins_installs = array();
3756 $all_themes_installs = array();
3757
3758 foreach ( $sites as $site ) {
3759 $blog_id = self::get_site_blog_id( $site );
3760
3761 $plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
3762
3763 foreach ( $plugins_installs as $slug => $install ) {
3764 if ( ! isset( $all_plugins_installs[ $slug ] ) ) {
3765 $all_plugins_installs[ $slug ] = array();
3766 }
3767
3768 $install->blog_id = $blog_id;
3769
3770 $all_plugins_installs[ $slug ][] = $install;
3771 }
3772
3773 $themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id );
3774
3775 foreach ( $themes_installs as $slug => $install ) {
3776 if ( ! isset( $all_themes_installs[ $slug ] ) ) {
3777 $all_themes_installs[ $slug ] = array();
3778 }
3779
3780 $install->blog_id = $blog_id;
3781
3782 $all_themes_installs[ $slug ][] = $install;
3783 }
3784 }
3785 }
3786
3787 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3788
3789 $vars = array(
3790 'plugin_sites' => $all_plugins_installs,
3791 'theme_sites' => $all_themes_installs,
3792 'users' => self::get_all_users(),
3793 'addons' => self::get_all_addons(),
3794 'account_addons' => self::get_all_account_addons(),
3795 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3796 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3797 );
3798
3799 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3800 fs_require_once_template( 'debug.php', $vars );
3801 }
3802
3803 #endregion
3804
3805 #----------------------------------------------------------------------------------
3806 #region Connectivity Issues
3807 #----------------------------------------------------------------------------------
3808
3809 /**
3810 * Check if Freemius should be turned on for the current plugin install.
3811 *
3812 * Note:
3813 * $this->_is_on is updated in has_api_connectivity()
3814 *
3815 * @author Vova Feldman (@svovaf)
3816 * @since 1.0.9
3817 *
3818 * @return bool
3819 */
3820 function is_on() {
3821 self::$_static_logger->entrance();
3822
3823 if ( isset( $this->_is_on ) ) {
3824 return $this->_is_on;
3825 }
3826
3827 // If already installed or pending then sure it's on :)
3828 if ( $this->is_registered() || $this->is_pending_activation() ) {
3829 $this->_is_on = true;
3830
3831 return true;
3832 }
3833
3834 return false;
3835 }
3836
3837 /**
3838 * @author Vova Feldman (@svovaf)
3839 * @since 1.1.7.3
3840 *
3841 * @param bool $flush_if_no_connectivity
3842 *
3843 * @return bool
3844 */
3845 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3846 if ( ! isset( $this->_storage->connectivity_test ) ) {
3847 // Connectivity test was never executed, or cache was cleared.
3848 return true;
3849 }
3850
3851 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3852 if ( WP_FS__IS_HTTP_REQUEST ) {
3853 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3854 // Domain changed.
3855 return true;
3856 }
3857
3858 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3859 // Server IP changed.
3860 return true;
3861 }
3862 }
3863 }
3864
3865 if ( $this->_storage->connectivity_test['is_connected'] &&
3866 $this->_storage->connectivity_test['is_active']
3867 ) {
3868 // API connected and Freemius is active - no need to run connectivity check.
3869 return false;
3870 }
3871
3872 if ( $flush_if_no_connectivity ) {
3873 /**
3874 * If explicitly asked to flush when no connectivity - do it only
3875 * if at least 10 sec passed from the last API connectivity test.
3876 */
3877 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3878 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3879 }
3880
3881 /**
3882 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3883 */
3884 $version = $this->get_plugin_version();
3885 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3886 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3887 return true;
3888 }
3889
3890 return false;
3891 }
3892
3893 /**
3894 * @author Vova Feldman (@svovaf)
3895 * @since 1.1.7.4
3896 *
3897 * @param int|null $blog_id Since 2.0.0.
3898 * @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test.
3899 *
3900 * @return object|false
3901 */
3902 private function ping( $blog_id = null, $is_gdpr_test = false ) {
3903 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
3904 return false;
3905 }
3906
3907 $version = $this->get_plugin_version();
3908
3909 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
3910
3911 return $this->get_api_plugin_scope()->ping(
3912 $this->get_anonymous_id( $blog_id ),
3913 array(
3914 'is_update' => json_encode( $is_update ),
3915 'version' => $version,
3916 'sdk' => $this->version,
3917 'is_admin' => json_encode( is_admin() ),
3918 'is_ajax' => json_encode( self::is_ajax() ),
3919 'is_cron' => json_encode( self::is_cron() ),
3920 'is_gdpr_test' => $is_gdpr_test,
3921 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
3922 )
3923 );
3924 }
3925
3926 /**
3927 * Check if there's any connectivity issue to Freemius API.
3928 *
3929 * @author Vova Feldman (@svovaf)
3930 * @since 1.0.9
3931 *
3932 * @param bool $flush_if_no_connectivity
3933 *
3934 * @return bool
3935 */
3936 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3937 $this->_logger->entrance();
3938
3939 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3940 return $this->_has_api_connection;
3941 }
3942
3943 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3944 isset( $this->_storage->connectivity_test ) &&
3945 true === $this->_storage->connectivity_test['is_connected']
3946 ) {
3947 unset( $this->_storage->connectivity_test );
3948 }
3949
3950 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3951 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3952 /**
3953 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3954 *
3955 * @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.
3956 */
3957 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3958 $this->is_premium() ||
3959 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3960
3961 return $this->_has_api_connection;
3962 }
3963
3964 $pong = $this->ping();
3965 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3966
3967 if ( ! $is_connected ) {
3968 // API failure.
3969 $this->_add_connectivity_issue_message( $pong );
3970 }
3971
3972 if ( $is_connected ) {
3973 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
3974 }
3975
3976 $this->store_connectivity_info( $pong, $is_connected );
3977
3978 return $this->_has_api_connection;
3979 }
3980
3981 /**
3982 * @author Vova Feldman (@svovaf)
3983 * @since 1.1.7.4
3984 *
3985 * @param object $pong
3986 * @param bool $is_connected
3987 */
3988 private function store_connectivity_info( $pong, $is_connected ) {
3989 $this->_logger->entrance();
3990
3991 $version = $this->get_plugin_version();
3992
3993 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3994 $is_active = false;
3995 } else {
3996 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3997 }
3998
3999 $is_active = $this->apply_filters(
4000 'is_on',
4001 $is_active,
4002 $this->is_plugin_update(),
4003 $version
4004 );
4005
4006 $this->_storage->connectivity_test = array(
4007 'is_connected' => $is_connected,
4008 'host' => $_SERVER['HTTP_HOST'],
4009 'server_ip' => WP_FS__REMOTE_ADDR,
4010 'is_active' => $is_active,
4011 'timestamp' => WP_FS__SCRIPT_START_TIME,
4012 // Last version with connectivity attempt.
4013 'version' => $version,
4014 );
4015
4016 $this->_has_api_connection = $is_connected;
4017 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4018 }
4019
4020 /**
4021 * Force turning Freemius on.
4022 *
4023 * @author Vova Feldman (@svovaf)
4024 * @since 1.1.8.1
4025 *
4026 * @return bool TRUE if successfully turned on.
4027 */
4028 private function turn_on() {
4029 $this->_logger->entrance();
4030
4031 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4032 return false;
4033 }
4034
4035 $updated_connectivity = $this->_storage->connectivity_test;
4036 $updated_connectivity['is_active'] = true;
4037 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4038 $this->_storage->connectivity_test = $updated_connectivity;
4039
4040 $this->_is_on = true;
4041
4042 return true;
4043 }
4044
4045 /**
4046 * Anonymous and unique site identifier (Hash).
4047 *
4048 * @author Vova Feldman (@svovaf)
4049 * @since 1.1.0
4050 *
4051 * @param null|int $blog_id Since 2.0.0
4052 *
4053 * @return string
4054 */
4055 function get_anonymous_id( $blog_id = null ) {
4056 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4057
4058 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4059 $key = fs_strip_url_protocol( get_site_url( $blog_id ) );
4060
4061 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4062 if ( empty( $secure_auth ) ||
4063 false !== strpos( $secure_auth, ' ' ) ||
4064 'put your unique phrase here' === $secure_auth
4065 ) {
4066 // Protect against default auth key.
4067 $secure_auth = md5( microtime() );
4068 }
4069
4070 /**
4071 * Base the unique identifier on the WP secure authentication key. Which
4072 * turns the key into a secret anonymous identifier. This will help us
4073 * to avoid duplicate installs generation on the backend upon opt-in.
4074 *
4075 * @author Vova Feldman (@svovaf)
4076 * @since 1.2.3
4077 */
4078 $unique_id = md5( $key . $secure_auth );
4079
4080 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4081 }
4082
4083 $this->_logger->departure( $unique_id );
4084
4085 return $unique_id;
4086 }
4087
4088 /**
4089 * @author Vova Feldman (@svovaf)
4090 * @since 1.1.7.4
4091 *
4092 * @return \WP_User
4093 */
4094 static function _get_current_wp_user() {
4095 self::require_pluggable_essentials();
4096 self::wp_cookie_constants();
4097
4098 return wp_get_current_user();
4099 }
4100
4101 /**
4102 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4103 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4104 * is network activated the cookie constants are only configured after the network
4105 * plugins activation, therefore, if we don't define those constants WP will throw
4106 * PHP warnings/notices.
4107 *
4108 * @author Vova Feldman (@svovaf)
4109 * @since 2.1.1
4110 */
4111 private static function wp_cookie_constants() {
4112 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4113 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4114 ) {
4115 return;
4116 }
4117
4118 /**
4119 * Used to guarantee unique hash cookies
4120 *
4121 * @since 1.5.0
4122 */
4123 if ( ! defined( 'COOKIEHASH' ) ) {
4124 $siteurl = get_site_option( 'siteurl' );
4125 if ( $siteurl ) {
4126 define( 'COOKIEHASH', md5( $siteurl ) );
4127 } else {
4128 define( 'COOKIEHASH', '' );
4129 }
4130 }
4131
4132 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4133 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4134 }
4135
4136 /**
4137 * @since 2.5.0
4138 */
4139 if ( ! defined( 'AUTH_COOKIE' ) ) {
4140 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4141 }
4142
4143 /**
4144 * @since 2.6.0
4145 */
4146 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4147 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4148 }
4149 }
4150
4151 /**
4152 * @author Vova Feldman (@svovaf)
4153 * @since 2.1.0
4154 *
4155 * @return int
4156 */
4157 static function get_current_wp_user_id() {
4158 $wp_user = self::_get_current_wp_user();
4159
4160 return $wp_user->ID;
4161 }
4162
4163 /**
4164 * @author Vova Feldman (@svovaf)
4165 * @since 1.2.1.7
4166 *
4167 * @param string $email
4168 *
4169 * @return bool
4170 */
4171 static function is_valid_email( $email ) {
4172 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4173 return false;
4174 }
4175
4176 $parts = explode( '@', $email );
4177
4178 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4179 return false;
4180 }
4181
4182 $blacklist = array(
4183 'admin.',
4184 'webmaster.',
4185 'localhost.',
4186 'dev.',
4187 'development.',
4188 'test.',
4189 'stage.',
4190 'staging.',
4191 );
4192
4193 // Make sure domain is not one of the blacklisted.
4194 foreach ( $blacklist as $invalid ) {
4195 if ( 0 === strpos( $parts[1], $invalid ) ) {
4196 return false;
4197 }
4198 }
4199
4200 // Get the UTF encoded domain name.
4201 $domain = idn_to_ascii( $parts[1] ) . '.';
4202
4203 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4204 }
4205
4206 /**
4207 * Generate API connectivity issue message.
4208 *
4209 * @author Vova Feldman (@svovaf)
4210 * @since 1.0.9
4211 *
4212 * @param mixed $api_result
4213 * @param bool $is_first_failure
4214 */
4215 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
4216 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
4217 // Don't add message if it's the free version and can run anonymously.
4218 return;
4219 }
4220
4221 if ( ! function_exists( 'wp_nonce_url' ) ) {
4222 require_once ABSPATH . 'wp-includes/functions.php';
4223 }
4224
4225 $current_user = self::_get_current_wp_user();
4226 // $admin_email = get_option( 'admin_email' );
4227 $admin_email = $current_user->user_email;
4228
4229 // Aliases.
4230 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
4231 $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' );
4232 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
4233 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
4234 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
4235 $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' );
4236 /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
4237 $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
4238 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
4239 $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' );
4240
4241 $message = false;
4242 if ( is_object( $api_result ) &&
4243 isset( $api_result->error ) &&
4244 isset( $api_result->error->code )
4245 ) {
4246 switch ( $api_result->error->code ) {
4247 case 'curl_missing':
4248 $missing_methods = '';
4249 if ( is_array( $api_result->missing_methods ) &&
4250 ! empty( $api_result->missing_methods )
4251 ) {
4252 foreach ( $api_result->missing_methods as $m ) {
4253 if ( 'curl_version' === $m ) {
4254 continue;
4255 }
4256
4257 if ( ! empty( $missing_methods ) ) {
4258 $missing_methods .= ', ';
4259 }
4260
4261 $missing_methods .= sprintf( '<code>%s</code>', $m );
4262 }
4263
4264 if ( ! empty( $missing_methods ) ) {
4265 $missing_methods = sprintf(
4266 '<br><br><b>%s</b> %s',
4267 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
4268 $missing_methods
4269 );
4270 }
4271 }
4272
4273 $message = sprintf(
4274 $x_requires_access_to_api . ' ' .
4275 $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' ) . ' ' .
4276 $missing_methods .
4277 ' %s',
4278 '<b>' . $this->get_plugin_name() . '</b>',
4279 sprintf(
4280 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4281 sprintf(
4282 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
4283 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
4284 ' - ' . sprintf(
4285 $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' ),
4286 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4287 )
4288 ),
4289 sprintf(
4290 '<b>%s</b> - %s',
4291 $sysadmin_title,
4292 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 ) ) )
4293 ),
4294 sprintf(
4295 '<a href="%s"><b>%s</b></a> - %s',
4296 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 ),
4297 $deactivate_plugin_title,
4298 $deactivate_plugin_desc
4299 )
4300 )
4301 );
4302 break;
4303 case 'cloudflare_ddos_protection':
4304 $message = sprintf(
4305 $x_requires_access_to_api . ' ' .
4306 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
4307 $happy_to_resolve_issue_asap .
4308 ' %s',
4309 '<b>' . $this->get_plugin_name() . '</b>',
4310 sprintf(
4311 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4312 sprintf(
4313 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
4314 $fix_issue_title,
4315 ' - ' . sprintf(
4316 $fix_issue_desc,
4317 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4318 )
4319 ),
4320 sprintf(
4321 '<a href="%s" target="_blank" rel="noopener noreferrer"><b>%s</b></a> - %s',
4322 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4323 $install_previous_title,
4324 $install_previous_desc
4325 ),
4326 sprintf(
4327 '<a href="%s"><b>%s</b></a> - %s',
4328 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 ),
4329 $deactivate_plugin_title,
4330 $deactivate_plugin_desc
4331 )
4332 )
4333 );
4334 break;
4335 case 'squid_cache_block':
4336 $message = sprintf(
4337 $x_requires_access_to_api . ' ' .
4338 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
4339 ' %s',
4340 '<b>' . $this->get_plugin_name() . '</b>',
4341 sprintf(
4342 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4343 sprintf(
4344 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
4345 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
4346 sprintf(
4347 $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' ),
4348 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4349 )
4350 ),
4351 sprintf(
4352 '<b>%s</b> - %s',
4353 $sysadmin_title,
4354 sprintf(
4355 $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' ),
4356 // We use a filter since the plugin might require additional API connectivity.
4357 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
4358 'api.freemius.com',
4359 'wp.freemius.com'
4360 ) ) ) . '</b>',
4361 $this->_module_type
4362 )
4363 ),
4364 sprintf(
4365 '<a href="%s"><b>%s</b></a> - %s',
4366 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 ),
4367 $deactivate_plugin_title,
4368 $deactivate_plugin_desc
4369 )
4370 )
4371 );
4372 break;
4373 // default:
4374 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
4375 // break;
4376 }
4377 }
4378
4379 $message_id = 'failed_connect_api';
4380 $type = 'error';
4381
4382 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
4383
4384 if ( false === $message ) {
4385 if ( $is_first_failure ) {
4386 // First attempt failed.
4387 $message = sprintf(
4388 $x_requires_access_to_api . ' ' .
4389 $connectivity_test_fails_message . ' ' .
4390 $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>' .
4391 '%s',
4392 '<b>' . $this->get_plugin_name() . '</b>',
4393 sprintf(
4394 '<div id="fs_firewall_issue_options">%s %s</div>',
4395 sprintf(
4396 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
4397 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
4398 ),
4399 sprintf(
4400 '<a href="%s" class="button">%s</a>',
4401 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 ),
4402 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
4403 )
4404 )
4405 );
4406
4407 $message_id = 'failed_connect_api_first';
4408 $type = 'promotion';
4409 } else {
4410 // Second connectivity attempt failed.
4411 $message = sprintf(
4412 $x_requires_access_to_api . ' ' .
4413 $connectivity_test_fails_message . ' ' .
4414 $happy_to_resolve_issue_asap .
4415 ' %s',
4416 '<b>' . $this->get_plugin_name() . '</b>',
4417 sprintf(
4418 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4419 sprintf(
4420 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
4421 $fix_issue_title,
4422 ' - ' . sprintf(
4423 $fix_issue_desc,
4424 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4425 )
4426 ),
4427 sprintf(
4428 '<a href="%s" target="_blank" rel="noopener noreferrer"><b>%s</b></a> - %s',
4429 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4430 $install_previous_title,
4431 $install_previous_desc
4432 ),
4433 sprintf(
4434 '<a href="%s"><b>%s</b></a> - %s',
4435 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 ),
4436 $deactivate_plugin_title,
4437 $deactivate_plugin_desc
4438 )
4439 )
4440 );
4441 }
4442 }
4443
4444 $this->_admin_notices->add_sticky(
4445 $message,
4446 $message_id,
4447 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4448 $type
4449 );
4450 }
4451
4452 /**
4453 * Handle user request to resolve connectivity issue.
4454 * This method will send an email to Freemius API technical staff for resolution.
4455 * The email will contain server's info and installed plugins (might be caching issue).
4456 *
4457 * @author Vova Feldman (@svovaf)
4458 * @since 1.0.9
4459 */
4460 function _email_about_firewall_issue() {
4461 check_admin_referer( 'fs_resolve_firewall_issues' );
4462
4463 if ( ! current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) ) {
4464 return;
4465 }
4466
4467 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
4468
4469 $pong = $this->ping();
4470
4471 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4472
4473 if ( $is_connected ) {
4474 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4475
4476 $this->store_connectivity_info( $pong, $is_connected );
4477
4478 echo $this->get_after_plugin_activation_redirect_url();
4479 exit;
4480 }
4481
4482 $current_user = self::_get_current_wp_user();
4483 $admin_email = $current_user->user_email;
4484
4485 $error_type = fs_request_get( 'error_type', 'general' );
4486
4487 switch ( $error_type ) {
4488 case 'squid':
4489 $title = 'Squid ACL Blocking Issue';
4490 break;
4491 case 'cloudflare':
4492 $title = 'CloudFlare Blocking Issue';
4493 break;
4494 default:
4495 $title = 'API Connectivity Issue';
4496 break;
4497 }
4498
4499 $custom_email_sections = array();
4500
4501 // Add 'API Error' custom email section.
4502 $custom_email_sections['api_error'] = array(
4503 'title' => 'API Error',
4504 'rows' => array(
4505 'ping' => array(
4506 'API Error',
4507 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
4508 ),
4509 )
4510 );
4511
4512 // Send email with technical details to resolve API connectivity issues.
4513 $this->send_email(
4514 'api@freemius.com', // recipient
4515 $title . ' [' . $this->get_plugin_name() . ']', // subject
4516 $custom_email_sections,
4517 array( "Reply-To: $admin_email <$admin_email>" ) // headers
4518 );
4519
4520 $this->_admin_notices->add_sticky(
4521 sprintf(
4522 $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' ),
4523 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4524 ),
4525 'server_details_sent'
4526 );
4527
4528 // Action was taken, tell that API connectivity troubleshooting should be off now.
4529
4530 echo "1";
4531 exit;
4532 }
4533
4534 /**
4535 * Handle connectivity test retry approved by the user.
4536 *
4537 * @author Vova Feldman (@svovaf)
4538 * @since 1.1.7.4
4539 */
4540 function _retry_connectivity_test() {
4541 check_admin_referer( 'fs_retry_connectivity_test' );
4542
4543 if ( ! current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) ) {
4544 return;
4545 }
4546
4547 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
4548
4549 $pong = $this->ping();
4550
4551 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4552
4553 if ( $is_connected ) {
4554 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4555
4556 $this->store_connectivity_info( $pong, $is_connected );
4557
4558 echo $this->get_after_plugin_activation_redirect_url();
4559 } else {
4560 // Add connectivity issue message after 2nd failed attempt.
4561 $this->_add_connectivity_issue_message( $pong, false );
4562
4563 echo "1";
4564 }
4565
4566 exit;
4567 }
4568
4569 static function _add_firewall_issues_javascript() {
4570 $params = array();
4571 fs_require_once_template( 'firewall-issues-js.php', $params );
4572 }
4573
4574 #endregion
4575
4576 #----------------------------------------------------------------------------------
4577 #region Email
4578 #----------------------------------------------------------------------------------
4579
4580 /**
4581 * Generates and sends an HTML email with customizable sections.
4582 *
4583 * @author Leo Fajardo (@leorw)
4584 * @since 1.1.2
4585 *
4586 * @param string $to_address
4587 * @param string $subject
4588 * @param array $sections
4589 * @param array $headers
4590 *
4591 * @return bool Whether the email contents were sent successfully.
4592 */
4593 private function send_email(
4594 $to_address,
4595 $subject,
4596 $sections = array(),
4597 $headers = array()
4598 ) {
4599 $default_sections = $this->get_email_sections();
4600
4601 // Insert new sections or replace the default email sections.
4602 if ( is_array( $sections ) && ! empty( $sections ) ) {
4603 foreach ( $sections as $section_id => $custom_section ) {
4604 if ( ! isset( $default_sections[ $section_id ] ) ) {
4605 // If the section does not exist, add it.
4606 $default_sections[ $section_id ] = $custom_section;
4607 } else {
4608 // If the section already exists, override it.
4609 $current_section = $default_sections[ $section_id ];
4610
4611 // Replace the current section's title if a custom section title exists.
4612 if ( isset( $custom_section['title'] ) ) {
4613 $current_section['title'] = $custom_section['title'];
4614 }
4615
4616 // Insert new rows under the current section or replace the default rows.
4617 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4618 foreach ( $custom_section['rows'] as $row_id => $row ) {
4619 $current_section['rows'][ $row_id ] = $row;
4620 }
4621 }
4622
4623 $default_sections[ $section_id ] = $current_section;
4624 }
4625 }
4626 }
4627
4628 $vars = array( 'sections' => $default_sections );
4629 $message = fs_get_template( 'email.php', $vars );
4630
4631 // Set the type of email to HTML.
4632 $headers[] = 'Content-type: text/html; charset=UTF-8';
4633
4634 $header_string = implode( "\r\n", $headers );
4635
4636 return wp_mail(
4637 $to_address,
4638 $subject,
4639 $message,
4640 $header_string
4641 );
4642 }
4643
4644 /**
4645 * Generates the data for the sections of the email content.
4646 *
4647 * @author Leo Fajardo (@leorw)
4648 * @since 1.1.2
4649 *
4650 * @return array
4651 */
4652 private function get_email_sections() {
4653 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4654 $current_user = self::_get_current_wp_user();
4655
4656 // Retrieve the cURL version information so that we can get the version number below.
4657 $curl_version_information = curl_version();
4658
4659 $active_plugin = self::get_active_plugins();
4660
4661 // Generate the list of active plugins separated by new line.
4662 $active_plugin_string = '';
4663 foreach ( $active_plugin as $plugin ) {
4664 $active_plugin_string .= sprintf(
4665 '<a href="%s">%s</a> [v%s]<br>',
4666 $plugin['PluginURI'],
4667 $plugin['Name'],
4668 $plugin['Version']
4669 );
4670 }
4671
4672 $server_ip = WP_FS__REMOTE_ADDR;
4673
4674 // Add PHP info for deeper investigation.
4675 ob_start();
4676 phpinfo();
4677 $php_info = ob_get_clean();
4678
4679 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4680
4681 // Generate the default email sections.
4682 $sections = array(
4683 'sdk' => array(
4684 'title' => 'SDK',
4685 'rows' => array(
4686 'fs_version' => array( 'FS Version', $this->version ),
4687 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4688 )
4689 ),
4690 'plugin' => array(
4691 'title' => ucfirst( $this->get_module_type() ),
4692 'rows' => array(
4693 'name' => array( 'Name', $this->get_plugin_name() ),
4694 'version' => array( 'Version', $this->get_plugin_version() )
4695 )
4696 ),
4697 'api' => array(
4698 'title' => 'API Subdomain',
4699 'rows' => array(
4700 'dns' => array(
4701 'DNS_CNAME',
4702 function_exists( 'dns_get_record' ) ?
4703 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4704 'dns_get_record() disabled/blocked'
4705 ),
4706 'ip' => array(
4707 'IP',
4708 function_exists( 'gethostbyname' ) ?
4709 gethostbyname( $api_domain ) :
4710 'gethostbyname() disabled/blocked'
4711 ),
4712 ),
4713 ),
4714 'site' => array(
4715 'title' => 'Site',
4716 'rows' => array(
4717 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4718 'address' => array( 'Address', site_url() ),
4719 'host' => array(
4720 'HTTP_HOST',
4721 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4722 ),
4723 'hosting' => array(
4724 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4725 fs_request_get( 'hosting_company' ) :
4726 'Unknown',
4727 ),
4728 'server_addr' => array(
4729 'SERVER_ADDR',
4730 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4731 )
4732 )
4733 ),
4734 'user' => array(
4735 'title' => 'User',
4736 'rows' => array(
4737 'email' => array( 'Email', $current_user->user_email ),
4738 'first' => array( 'First', $current_user->user_firstname ),
4739 'last' => array( 'Last', $current_user->user_lastname )
4740 )
4741 ),
4742 'plugins' => array(
4743 'title' => 'Plugins',
4744 'rows' => array(
4745 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4746 )
4747 ),
4748 'php_info' => array(
4749 'title' => 'PHP Info',
4750 'rows' => array(
4751 'info' => array( $php_info )
4752 ),
4753 )
4754 );
4755
4756 // Allow the sections to be modified by other code.
4757 $sections = $this->apply_filters( 'email_template_sections', $sections );
4758
4759 return $sections;
4760 }
4761
4762 #endregion
4763
4764 #----------------------------------------------------------------------------------
4765 #region Initialization
4766 #----------------------------------------------------------------------------------
4767
4768 /**
4769 * Init plugin's Freemius instance.
4770 *
4771 * @author Vova Feldman (@svovaf)
4772 * @since 1.0.1
4773 *
4774 * @param number $id
4775 * @param string $public_key
4776 * @param bool $is_live
4777 * @param bool $is_premium
4778 */
4779 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4780 $this->_logger->entrance();
4781
4782 $this->dynamic_init( array(
4783 'id' => $id,
4784 'public_key' => $public_key,
4785 'is_live' => $is_live,
4786 'is_premium' => $is_premium,
4787 ) );
4788 }
4789
4790 /**
4791 * Dynamic initiator, originally created to support initiation
4792 * with parent_id for add-ons.
4793 *
4794 * @author Vova Feldman (@svovaf)
4795 * @since 1.0.6
4796 *
4797 * @param array $plugin_info
4798 *
4799 * @throws Freemius_Exception
4800 */
4801 function dynamic_init( array $plugin_info ) {
4802 $this->_logger->entrance();
4803
4804 $this->parse_settings( $plugin_info );
4805
4806 $this->register_after_settings_parse_hooks();
4807
4808 if ( $this->should_stop_execution() ) {
4809 return;
4810 }
4811
4812 if ( ! $this->is_registered() ) {
4813 if ( $this->is_anonymous() ) {
4814 // If user skipped, no need to test connectivity.
4815 $this->_has_api_connection = true;
4816 $this->_is_on = true;
4817 } else {
4818 if ( ! $this->has_api_connectivity() ) {
4819 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
4820 $this->_admin_notices->has_sticky( 'failed_connect_api' )
4821 ) {
4822 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
4823 // If anonymous mode is disabled, add firewall admin-notice message.
4824 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
4825
4826 $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' );
4827 add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array(
4828 &$this,
4829 '_email_about_firewall_issue'
4830 ) );
4831
4832 add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array(
4833 &$this,
4834 '_retry_connectivity_test'
4835 ) );
4836
4837 /**
4838 * 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.
4839 *
4840 * @author Vova Feldman (@svovaf)
4841 * @since 2.0.0
4842 */
4843 /*$this->add_ajax_action( 'resolve_firewall_issues', array(
4844 &$this,
4845 '_email_about_firewall_issue'
4846 ) );
4847
4848 $this->add_ajax_action( 'retry_connectivity_test', array(
4849 &$this,
4850 '_retry_connectivity_test'
4851 ) );*/
4852 }
4853 }
4854
4855 return;
4856 } else {
4857 $this->_admin_notices->remove_sticky( array(
4858 'failed_connect_api_first',
4859 'failed_connect_api',
4860 ) );
4861
4862 if ( $this->_anonymous_mode ) {
4863 // Simulate anonymous mode.
4864 $this->_is_anonymous = true;
4865 }
4866 }
4867 }
4868 }
4869
4870 /**
4871 * This should be executed even if Freemius is off for the core module,
4872 * otherwise, the add-ons dialogbox won't work properly. This is esepcially
4873 * relevant when the developer decided to turn FS off for existing users.
4874 *
4875 * @author Vova Feldman (@svovaf)
4876 */
4877 if ( $this->is_user_in_admin() &&
4878 'plugin-information' === fs_request_get( 'tab', false ) &&
4879 $this->should_use_freemius_updater_and_dialog() &&
4880 (
4881 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4882 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4883 )
4884 ) {
4885 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4886
4887 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4888 }
4889
4890 // Check if Freemius is on for the current plugin.
4891 // This MUST be executed after all the plugin variables has been loaded.
4892 if ( ! $this->is_registered() && ! $this->is_on() ) {
4893 return;
4894 }
4895
4896 if ( $this->has_api_connectivity() ) {
4897 if ( self::is_cron() ) {
4898 $this->hook_callback_to_sync_cron();
4899 } else if ( $this->is_user_in_admin() ) {
4900 /**
4901 * Schedule daily data sync cron if:
4902 *
4903 * 1. User opted-in (for tracking).
4904 * 2. If skipped, but later upgraded (opted-in via upgrade).
4905 *
4906 * @author Vova Feldman (@svovaf)
4907 * @since 1.1.7.3
4908 *
4909 */
4910 if ( $this->is_registered() ) {
4911 if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) {
4912 $this->schedule_sync_cron();
4913 }
4914 }
4915
4916 /**
4917 * Check if requested for manual blocking background sync.
4918 */
4919 if ( fs_request_has( 'background_sync' ) ) {
4920 $this->run_manual_sync();
4921 }
4922 }
4923 }
4924
4925 if ( $this->is_registered() ) {
4926 $this->hook_callback_to_install_sync();
4927 }
4928
4929 if ( $this->is_addon() ) {
4930 if ( $this->is_parent_plugin_installed() ) {
4931 // Link to parent FS.
4932 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4933
4934 // Get parent plugin reference.
4935 $this->_parent_plugin = $this->_parent->get_plugin();
4936 }
4937 }
4938
4939 if ( $this->is_user_in_admin() ) {
4940 if ( $this->is_addon() ) {
4941 if ( ! $this->is_parent_plugin_installed() ) {
4942 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4943
4944 if ( isset( $plugin_info['parent'] ) ) {
4945 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4946 }
4947
4948 $this->_admin_notices->add(
4949 ( ! empty( $parent_name ) ?
4950 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 ) :
4951 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() )
4952 ),
4953 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4954 'error'
4955 );
4956
4957 return;
4958 } else {
4959 $is_network_admin = fs_is_network_admin();
4960
4961 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4962 // If add-on activated and parent not, automatically install parent for the user.
4963 $this->activate_parent_account( $this->_parent );
4964 } else if (
4965 $this->_parent->is_registered() &&
4966 ! $this->is_registered() &&
4967 /**
4968 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4969 * * Network active and in network admin - network activate add-on account.
4970 * * Network active and not in network admin - activate add-on account for the current blog.
4971 * * Not network active and not in network admin - activate add-on account for the current blog.
4972 *
4973 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4974 *
4975 * @author Leo Fajardo (@leorw)
4976 * @since 2.3.0
4977 */
4978 ( $this->is_network_active() || ! $is_network_admin )
4979 ) {
4980 $premium_license = null;
4981
4982 if (
4983 ! $this->has_free_plan() &&
4984 $this->is_bundle_license_auto_activation_enabled() &&
4985 $this->_parent->is_activated_with_bundle_license()
4986 ) {
4987 /**
4988 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4989 *
4990 * @author Leo Fajardo (@leorw)
4991 * @since 2.4.0
4992 */
4993 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4994
4995 if (
4996 is_object( $bundle_license ) &&
4997 ! empty( $bundle_license->products ) &&
4998 in_array( $this->get_id(), $bundle_license->products )
4999 ) {
5000 $premium_license = $bundle_license;
5001 }
5002 }
5003
5004 if ( $this->has_free_plan() || is_object( $premium_license) ) {
5005 // If parent plugin activated, automatically install add-on for the user.
5006 $this->_activate_addon_account(
5007 $this->_parent,
5008 ( $this->is_network_active() && $is_network_admin ) ?
5009 true :
5010 get_current_blog_id(),
5011 $premium_license
5012 );
5013 }
5014 }
5015
5016 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
5017 if ( $this->is_premium() ) {
5018 // Remove add-on download admin-notice.
5019 $this->_parent->_admin_notices->remove_sticky( array(
5020 'addon_plan_upgraded_' . $this->_slug,
5021 'no_addon_license_' . $this->_slug,
5022 ) );
5023 }
5024
5025 // $this->deactivate_premium_only_addon_without_license();
5026 }
5027 }
5028
5029 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
5030
5031 // if ( $this->is_registered() ||
5032 // $this->is_anonymous() ||
5033 // $this->is_pending_activation()
5034 // ) {
5035 // $this->_init_admin();
5036 // }
5037 }
5038
5039 /**
5040 * Should be called outside `$this->is_user_in_admin()` scope
5041 * because the updater has some logic that needs to be executed
5042 * during AJAX calls.
5043 *
5044 * Currently we need to hook to the `http_request_host_is_external` filter.
5045 * In the future, there might be additional logic added.
5046 *
5047 * @author Vova Feldman
5048 * @since 1.2.1.6
5049 */
5050 if (
5051 $this->should_use_freemius_updater_and_dialog() &&
5052 (
5053 $this->is_premium() ||
5054 /**
5055 * If not premium but the premium version is installed, also instantiate the updater so that the
5056 * plugin information dialog of the premium version will have the information from the server.
5057 *
5058 * @author Leo Fajardo (@leorw)
5059 * @since 2.2.3
5060 */
5061 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
5062 ) &&
5063 $this->has_release_on_freemius()
5064 ) {
5065 FS_Plugin_Updater::instance( $this );
5066 }
5067
5068 $this->do_action( 'initiated' );
5069
5070 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
5071 if ( isset( $this->_storage->prev_is_premium ) ) {
5072 $this->apply_filters(
5073 'after_code_type_change',
5074 // New code type.
5075 $this->_plugin->is_premium
5076 );
5077 } else {
5078 // Set for code type for the first time.
5079 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5080 }
5081 }
5082
5083 if ( ! $this->is_addon() ) {
5084 if ( $this->is_registered() ) {
5085 // Fix for upgrade from versions < 1.0.9.
5086 if ( ! isset( $this->_storage->activation_timestamp ) ) {
5087 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
5088 }
5089
5090 $this->do_action( 'after_init_plugin_registered' );
5091 } else if ( $this->is_anonymous() ) {
5092 $this->do_action( 'after_init_plugin_anonymous' );
5093 } else if ( $this->is_pending_activation() ) {
5094 $this->do_action( 'after_init_plugin_pending_activations' );
5095 }
5096 } else {
5097 if ( $this->is_registered() ) {
5098 $this->do_action( 'after_init_addon_registered' );
5099 } else if ( $this->is_anonymous() ) {
5100 $this->do_action( 'after_init_addon_anonymous' );
5101 } else if ( $this->is_pending_activation() ) {
5102 $this->do_action( 'after_init_addon_pending_activations' );
5103 }
5104 }
5105 }
5106
5107 /**
5108 * @author Leo Fajardo (@leorw)
5109 * @since 2.2.3
5110 *
5111 * @return bool
5112 */
5113 private function should_use_freemius_updater_and_dialog() {
5114 return (
5115 /**
5116 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5117 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5118 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5119 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5120 * plugin details from .org).
5121 */
5122 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5123 (
5124 ! self::is_plugin_install_page() &&
5125 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5126 ( 'install-plugin' !== fs_request_get( 'action' ) )
5127 )
5128 );
5129 }
5130
5131 /**
5132 * @author Leo Fajardo (@leorw)
5133 *
5134 * @since 1.2.1.5
5135 */
5136 function _stop_tracking_callback() {
5137 $this->_logger->entrance();
5138
5139 $this->check_ajax_referer( 'stop_tracking' );
5140
5141 $result = $this->stop_tracking( fs_is_network_admin() );
5142
5143 if ( true === $result ) {
5144 self::shoot_ajax_success();
5145 }
5146
5147 $this->_logger->api_error( $result );
5148
5149 self::shoot_ajax_failure(
5150 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
5151 ( $this->is_api_error( $result ) && isset( $result->error ) ?
5152 $result->error->message :
5153 var_export( $result, true ) )
5154 );
5155 }
5156
5157 /**
5158 * @author Leo Fajardo (@leorw)
5159 * @since 1.2.1.5
5160 */
5161 function _allow_tracking_callback() {
5162 $this->_logger->entrance();
5163
5164 $this->check_ajax_referer( 'allow_tracking' );
5165
5166 $result = $this->allow_tracking( fs_is_network_admin() );
5167
5168 if ( true === $result ) {
5169 self::shoot_ajax_success();
5170 }
5171
5172 $this->_logger->api_error( $result );
5173
5174 self::shoot_ajax_failure(
5175 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
5176 ( $this->is_api_error( $result ) && isset( $result->error ) ?
5177 $result->error->message :
5178 var_export( $result, true ) )
5179 );
5180 }
5181
5182 /**
5183 * Opt-out from usage tracking.
5184 *
5185 * Note: This will not delete the account information but will stop all tracking.
5186 *
5187 * Returns:
5188 * 1. FALSE - If the user never opted-in.
5189 * 2. TRUE - If successfully opted-out.
5190 * 3. object - API result on failure.
5191 *
5192 * @author Leo Fajardo (@leorw)
5193 * @since 1.2.1.5
5194 *
5195 * @return bool|object
5196 */
5197 function stop_site_tracking() {
5198 $this->_logger->entrance();
5199
5200 if ( ! $this->is_registered() ) {
5201 // User never opted-in.
5202 return false;
5203 }
5204
5205 if ( $this->is_tracking_prohibited() ) {
5206 // Already disconnected.
5207 return true;
5208 }
5209
5210 // Send update to FS.
5211 $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array(
5212 'is_disconnected' => true
5213 ) );
5214
5215 if ( ! $this->is_api_result_entity( $result ) ||
5216 ! isset( $result->is_disconnected ) ||
5217 ! $result->is_disconnected
5218 ) {
5219 $this->_logger->api_error( $result );
5220
5221 return $result;
5222 }
5223
5224 $this->_site->is_disconnected = $result->is_disconnected;
5225 $this->_store_site();
5226
5227 $this->clear_sync_cron();
5228
5229 // Successfully disconnected.
5230 return true;
5231 }
5232
5233 /**
5234 * Opt-out network from usage tracking.
5235 *
5236 * Note: This will not delete the account information but will stop all tracking.
5237 *
5238 * Returns:
5239 * 1. FALSE - If the user never opted-in.
5240 * 2. TRUE - If successfully opted-out.
5241 * 3. object - API result on failure.
5242 *
5243 * @author Leo Fajardo (@leorw)
5244 * @since 1.2.1.5
5245 *
5246 * @return bool|object
5247 */
5248 function stop_network_tracking() {
5249 $this->_logger->entrance();
5250
5251 if ( ! $this->is_registered() ) {
5252 // User never opted-in.
5253 return false;
5254 }
5255
5256 $install_id_2_blog_id = array();
5257 $installs_map = $this->get_blog_install_map();
5258
5259 $opt_out_all = true;
5260
5261 $params = array();
5262 foreach ( $installs_map as $blog_id => $install ) {
5263 if ( $install->is_tracking_prohibited() ) {
5264 // Already opted-out.
5265 continue;
5266 }
5267
5268 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5269 // Opt-out only from non-delegated installs.
5270 $opt_out_all = false;
5271 continue;
5272 }
5273
5274 $params[] = array( 'id' => $install->id );
5275
5276 $install_id_2_blog_id[ $install->id ] = $blog_id;
5277 }
5278
5279 if ( empty( $install_id_2_blog_id ) ) {
5280 return true;
5281 }
5282
5283 $params[] = array( 'is_disconnected' => true );
5284
5285 // Send update to FS.
5286 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5287
5288 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5289 $this->_logger->api_error( $result );
5290
5291 return $result;
5292 }
5293
5294 foreach ( $result->installs as $r_install ) {
5295 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5296 $install = $installs_map[ $blog_id ];
5297 $install->is_disconnected = $r_install->is_disconnected;
5298 $this->_store_site( true, $blog_id, $install );
5299 }
5300
5301 $this->clear_sync_cron( $opt_out_all );
5302
5303 // Successfully disconnected.
5304 return true;
5305 }
5306
5307 /**
5308 * Opt-out from usage tracking.
5309 *
5310 * Note: This will not delete the account information but will stop all tracking.
5311 *
5312 * Returns:
5313 * 1. FALSE - If the user never opted-in.
5314 * 2. TRUE - If successfully opted-out.
5315 * 3. object - API result on failure.
5316 *
5317 * @author Leo Fajardo (@leorw)
5318 * @since 1.2.1.5
5319 *
5320 * @param bool $is_network_action
5321 *
5322 * @return bool|object
5323 */
5324 function stop_tracking( $is_network_action = false ) {
5325 $this->_logger->entrance();
5326
5327 return $is_network_action ?
5328 $this->stop_network_tracking() :
5329 $this->stop_site_tracking();
5330 }
5331
5332 /**
5333 * Opt-in back into usage tracking.
5334 *
5335 * Note: This will only work if the user opted-in previously.
5336 *
5337 * Returns:
5338 * 1. FALSE - If the user never opted-in.
5339 * 2. TRUE - If successfully opted-in back to usage tracking.
5340 * 3. object - API result on failure.
5341 *
5342 * @author Leo Fajardo (@leorw)
5343 * @since 1.2.1.5
5344 *
5345 * @return bool|object
5346 */
5347 function allow_site_tracking() {
5348 $this->_logger->entrance();
5349
5350 if ( ! $this->is_registered() ) {
5351 // User never opted-in.
5352 return false;
5353 }
5354
5355 if ( $this->is_tracking_allowed() ) {
5356 // Tracking already allowed.
5357 return true;
5358 }
5359
5360 $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array(
5361 'is_disconnected' => false
5362 ) );
5363
5364 if ( ! $this->is_api_result_entity( $result ) ||
5365 ! isset( $result->is_disconnected ) ||
5366 $result->is_disconnected
5367 ) {
5368 $this->_logger->api_error( $result );
5369
5370 return $result;
5371 }
5372
5373 $this->_site->is_disconnected = $result->is_disconnected;
5374 $this->_store_site();
5375
5376 $this->schedule_sync_cron();
5377
5378 // Successfully reconnected.
5379 return true;
5380 }
5381
5382 /**
5383 * Opt-in network back into usage tracking.
5384 *
5385 * Note: This will only work if the user opted-in previously.
5386 *
5387 * Returns:
5388 * 1. FALSE - If the user never opted-in.
5389 * 2. TRUE - If successfully opted-in back to usage tracking.
5390 * 3. object - API result on failure.
5391 *
5392 * @author Leo Fajardo (@leorw)
5393 * @since 1.2.1.5
5394 *
5395 * @return bool|object
5396 */
5397 function allow_network_tracking() {
5398 $this->_logger->entrance();
5399
5400 if ( ! $this->is_registered() ) {
5401 // User never opted-in.
5402 return false;
5403 }
5404
5405 $install_id_2_blog_id = array();
5406 $installs_map = $this->get_blog_install_map();
5407
5408 $params = array();
5409 foreach ( $installs_map as $blog_id => $install ) {
5410 if ( $install->is_tracking_allowed() ) {
5411 continue;
5412 }
5413
5414 $params[] = array( 'id' => $install->id );
5415
5416 $install_id_2_blog_id[ $install->id ] = $blog_id;
5417 }
5418
5419 if ( empty( $install_id_2_blog_id ) ) {
5420 return true;
5421 }
5422
5423 $params[] = array( 'is_disconnected' => false );
5424
5425 // Send update to FS.
5426 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5427
5428
5429 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5430 $this->_logger->api_error( $result );
5431
5432 return $result;
5433 }
5434
5435 foreach ( $result->installs as $r_install ) {
5436 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5437 $install = $installs_map[ $blog_id ];
5438 $install->is_disconnected = $r_install->is_disconnected;
5439 $this->_store_site( true, $blog_id, $install );
5440 }
5441
5442 $this->schedule_sync_cron();
5443
5444 // Successfully reconnected.
5445 return true;
5446 }
5447
5448 /**
5449 * Opt-in back into usage tracking.
5450 *
5451 * Note: This will only work if the user opted-in previously.
5452 *
5453 * Returns:
5454 * 1. FALSE - If the user never opted-in.
5455 * 2. TRUE - If successfully opted-in back to usage tracking.
5456 * 3. object - API result on failure.
5457 *
5458 * @author Leo Fajardo (@leorw)
5459 * @since 1.2.1.5
5460 *
5461 * @param bool $is_network_action
5462 *
5463 * @return bool|object
5464 */
5465 function allow_tracking( $is_network_action = false ) {
5466 $this->_logger->entrance();
5467
5468 return $is_network_action ?
5469 $this->allow_network_tracking() :
5470 $this->allow_site_tracking();
5471 }
5472
5473 /**
5474 * If user opted-in and later disabled usage-tracking,
5475 * re-allow tracking for licensing and updates.
5476 *
5477 * @author Leo Fajardo (@leorw)
5478 * @since 1.2.1.5
5479 *
5480 * @param bool $is_context_single_site
5481 */
5482 private function reconnect_locally( $is_context_single_site = false ) {
5483 $this->_logger->entrance();
5484
5485 if ( ! $this->is_registered() ) {
5486 return;
5487 }
5488
5489 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5490 if ( $this->is_tracking_prohibited() ) {
5491 $this->_site->is_disconnected = false;
5492 $this->_store_site();
5493 }
5494 } else {
5495 $installs_map = $this->get_blog_install_map();
5496 foreach ( $installs_map as $blog_id => $install ) {
5497 /**
5498 * @var FS_Site $install
5499 */
5500 if ( $install->is_tracking_prohibited() ) {
5501 $install->is_disconnected = false;
5502 $this->_store_site( true, $blog_id, $install );
5503 }
5504 }
5505 }
5506 }
5507
5508 /**
5509 * @author Vova Feldman (@svovaf)
5510 * @since 2.3.2
5511 *
5512 * @return bool
5513 */
5514 function is_extensions_tracking_allowed() {
5515 return ( true === $this->apply_filters(
5516 'is_extensions_tracking_allowed',
5517 $this->_storage->get( 'is_extensions_tracking_allowed', null )
5518 ) );
5519 }
5520
5521 /**
5522 * @author Vova Feldman (@svovaf)
5523 * @since 2.3.2
5524 */
5525 function _update_tracking_permission_callback() {
5526 $this->_logger->entrance();
5527
5528 $this->check_ajax_referer( 'update_tracking_permission' );
5529
5530 $is_enabled = fs_request_get_bool( 'is_enabled', null );
5531
5532 if ( ! is_bool( $is_enabled ) ) {
5533 self::shoot_ajax_failure();
5534 }
5535
5536 $permission = fs_request_get( 'permission' );
5537
5538 switch ( $permission ) {
5539 case 'extensions':
5540 $this->update_extensions_tracking_flag( $is_enabled );
5541 break;
5542 default:
5543 $permission = 'no_match';
5544 }
5545
5546 if ( 'no_match' === $permission ) {
5547 self::shoot_ajax_failure();
5548 }
5549
5550 self::shoot_ajax_success( array(
5551 'permissions' => array(
5552 $permission => $is_enabled,
5553 )
5554 ) );
5555 }
5556
5557 /**
5558 * @author Leo Fajardo (@leorw)
5559 * @since 2.3.2
5560 *
5561 * @param bool|null $is_enabled
5562 */
5563 function update_extensions_tracking_flag( $is_enabled ) {
5564 if ( is_bool( $is_enabled ) ) {
5565 $this->_storage->store( 'is_extensions_tracking_allowed', $is_enabled );
5566 }
5567 }
5568
5569 /**
5570 * Parse plugin's settings (as defined by the plugin dev).
5571 *
5572 * @author Vova Feldman (@svovaf)
5573 * @since 1.1.7.3
5574 *
5575 * @param array $plugin_info
5576 *
5577 * @throws \Freemius_Exception
5578 */
5579 private function parse_settings( &$plugin_info ) {
5580 $this->_logger->entrance();
5581
5582 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5583 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5584 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5585 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5586 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5587
5588 /**
5589 * @author Vova Feldman (@svovaf)
5590 * @since 1.1.9 Try to pull secret key from external config.
5591 */
5592 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5593 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5594 }
5595
5596 if ( isset( $plugin_info['parent'] ) ) {
5597 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5598 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5599 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5600 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5601 }
5602
5603 if ( false === $id ) {
5604 throw new Freemius_Exception( array(
5605 'error' => array(
5606 'type' => 'ParameterNotSet',
5607 'message' => 'Plugin id parameter is not set.',
5608 'code' => 'plugin_id_not_set',
5609 'http' => 500,
5610 )
5611 ) );
5612 }
5613 if ( false === $public_key ) {
5614 throw new Freemius_Exception( array(
5615 'error' => array(
5616 'type' => 'ParameterNotSet',
5617 'message' => 'Plugin public_key parameter is not set.',
5618 'code' => 'plugin_public_key_not_set',
5619 'http' => 500,
5620 )
5621 ) );
5622 }
5623
5624 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5625 $this->_plugin :
5626 new FS_Plugin();
5627
5628 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5629
5630 $plugin->update( array(
5631 'id' => $id,
5632 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5633 'public_key' => $public_key,
5634 'slug' => $this->_slug,
5635 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5636 'parent_plugin_id' => $parent_id,
5637 'version' => $this->get_plugin_version(),
5638 'title' => $this->get_plugin_name( $premium_suffix ),
5639 'file' => $this->_plugin_basename,
5640 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5641 'premium_suffix' => $premium_suffix,
5642 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5643 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5644 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5645 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5646 ) );
5647
5648 if ( $plugin->is_updated() ) {
5649 // Update plugin details.
5650 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5651 }
5652 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5653 $this->_plugin->secret_key = $secret_key;
5654
5655 /**
5656 * 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).
5657 *
5658 * @author Vova Feldman
5659 * @since 2.4.5
5660 */
5661 if ( $this->is_network_active() && fs_is_network_admin() ) {
5662 if ( isset( $plugin_info['menu_network'] ) &&
5663 is_array( $plugin_info['menu_network'] ) &&
5664 ! empty( $plugin_info['menu_network'] )
5665 ) {
5666 $plugin_info['menu'] = $plugin_info['menu_network'];
5667 }
5668 }
5669
5670 if ( ! isset( $plugin_info['menu'] ) ) {
5671 $plugin_info['menu'] = array();
5672
5673 if ( ! empty( $this->_storage->sdk_last_version ) &&
5674 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5675 ) {
5676 // Backward compatibility to 1.1.2
5677 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5678 $plugin_info['menu_slug'] :
5679 $this->_slug;
5680 }
5681 }
5682
5683 $this->_menu = FS_Admin_Menu_Manager::instance(
5684 $this->_module_id,
5685 $this->_module_type,
5686 $this->get_unique_affix()
5687 );
5688
5689 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5690
5691 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5692 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5693 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5694 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5695 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5696 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5697 if ( $this->_is_premium_only ) {
5698 // If premium only plugin, disable anonymous mode.
5699 $this->_enable_anonymous = false;
5700 $this->_anonymous_mode = false;
5701 } else {
5702 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5703 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5704 }
5705 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5706 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5707
5708 if ( ! empty( $plugin_info['trial'] ) ) {
5709 $this->_trial_days = $this->get_numeric_option(
5710 $plugin_info['trial'],
5711 'days',
5712 // Default to 0 - trial without days specification.
5713 0
5714 );
5715
5716 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5717 }
5718
5719 $this->_navigation = $this->get_option(
5720 $plugin_info,
5721 'navigation',
5722 $this->is_free_wp_org_theme() ?
5723 self::NAVIGATION_TABS :
5724 self::NAVIGATION_MENU
5725 );
5726 }
5727
5728 /**
5729 * @param string[] $options
5730 * @param string $key
5731 * @param mixed $default
5732 *
5733 * @return bool
5734 */
5735 private function get_option( &$options, $key, $default = false ) {
5736 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5737 }
5738
5739 private function get_bool_option( &$options, $key, $default = false ) {
5740 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5741 }
5742
5743 private function get_numeric_option( &$options, $key, $default = false ) {
5744 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5745 }
5746
5747 /**
5748 * Gate keeper.
5749 *
5750 * @author Vova Feldman (@svovaf)
5751 * @since 1.1.7.3
5752 *
5753 * @return bool
5754 */
5755 private function should_stop_execution() {
5756 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5757 /**
5758 * Don't execute Freemius until plugin was fully loaded at least once,
5759 * to give the opportunity for the activation hook to run before pinging
5760 * the API for connectivity test. This logic is relevant for the
5761 * identification of new plugin install vs. plugin update.
5762 *
5763 * @author Vova Feldman (@svovaf)
5764 * @since 1.1.9
5765 */
5766 return true;
5767 }
5768
5769 if ( $this->is_activation_mode() ) {
5770 if ( ! is_admin() ) {
5771 /**
5772 * If in activation mode, don't execute Freemius outside of the
5773 * admin dashboard.
5774 *
5775 * @author Vova Feldman (@svovaf)
5776 * @since 1.1.7.3
5777 */
5778 return true;
5779 }
5780
5781 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5782 /**
5783 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5784 * then don't start Freemius.
5785 *
5786 * @author Vova Feldman (@svovaf)
5787 * @since 1.1.6.3
5788 *
5789 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5790 */
5791 return true;
5792 }
5793
5794 if ( self::is_cron() ) {
5795 /**
5796 * If in activation mode, don't execute Freemius during wp crons
5797 * (wp crons have HTTP context - called as HTTP request).
5798 *
5799 * @author Vova Feldman (@svovaf)
5800 * @since 1.1.7.3
5801 */
5802 return true;
5803 }
5804
5805 if ( self::is_ajax() &&
5806 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
5807 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
5808 ) {
5809 /**
5810 * During activation, if running in AJAX mode, unless there's a sticky
5811 * connectivity issue notice, don't run Freemius.
5812 *
5813 * @author Vova Feldman (@svovaf)
5814 * @since 1.1.7.3
5815 */
5816 return true;
5817 }
5818 }
5819
5820 return false;
5821 }
5822
5823 /**
5824 * Triggered after code type has changed.
5825 *
5826 * @author Vova Feldman (@svovaf)
5827 * @since 1.1.9.1
5828 */
5829 function _after_code_type_change() {
5830 $this->_logger->entrance();
5831
5832 if ( $this->is_theme() ) {
5833 // Expire the cache of the previous tabs since the theme may
5834 // have setting updates after code type has changed.
5835 $this->_cache->expire( 'tabs' );
5836 $this->_cache->expire( 'tabs_stylesheets' );
5837 }
5838
5839 if ( $this->is_registered() ) {
5840 if ( ! $this->is_addon() ) {
5841 add_action(
5842 is_admin() ? 'admin_init' : 'init',
5843 array( &$this, '_plugin_code_type_changed' )
5844 );
5845 }
5846
5847 if ( $this->is_premium() ) {
5848 // Purge cached payments after switching to the premium version.
5849 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5850 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5851 }
5852 }
5853 }
5854
5855 /**
5856 * Handles plugin's code type change (free <--> premium).
5857 *
5858 * @author Vova Feldman (@svovaf)
5859 * @since 1.0.9
5860 */
5861 function _plugin_code_type_changed() {
5862 $this->_logger->entrance();
5863
5864 if ( $this->is_premium() ) {
5865 $this->reconnect_locally();
5866
5867 // Activated premium code.
5868 $this->do_action( 'after_premium_version_activation' );
5869
5870 // Remove all sticky messages related to download of the premium version.
5871 $this->_admin_notices->remove_sticky( array(
5872 'trial_started',
5873 'plan_upgraded',
5874 'plan_changed',
5875 'license_activated',
5876 ) );
5877
5878 $notice = '';
5879 if ( ! $this->is_only_premium() ) {
5880 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5881 }
5882
5883 $license_notice = $this->get_license_network_activation_notice();
5884 if ( ! empty( $license_notice ) ) {
5885 $notice .= ' ' . $license_notice;
5886 }
5887
5888 if ( ! empty( $notice ) ) {
5889 $this->_admin_notices->add_sticky(
5890 trim( $notice ),
5891 'premium_activated',
5892 $this->get_text_x_inline( 'W00t',
5893 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5894 );
5895 }
5896 } else {
5897 // Remove sticky message related to premium code activation.
5898 $this->_admin_notices->remove_sticky( 'premium_activated' );
5899
5900 // Activated free code (after had the premium before).
5901 $this->do_action( 'after_free_version_reactivation' );
5902
5903 if ( $this->is_paying() && ! $this->is_premium() ) {
5904 $this->_admin_notices->add_sticky(
5905 sprintf(
5906 /* translators: %s: License type (e.g. you have a professional license) */
5907 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5908 $this->get_plan_title()
5909 ) . $this->get_complete_upgrade_instructions(),
5910 'plan_upgraded',
5911 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
5912 );
5913 }
5914 }
5915
5916 // Schedule code type changes event.
5917 $this->schedule_install_sync();
5918
5919 /**
5920 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5921 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5922 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5923 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5924 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5925 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5926 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5927 */
5928 $this->unregister_uninstall_hook();
5929
5930 $this->clear_module_main_file_cache();
5931
5932 // Update is_premium of latest version.
5933 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5934 }
5935
5936 #endregion
5937
5938 #----------------------------------------------------------------------------------
5939 #region Add-ons
5940 #----------------------------------------------------------------------------------
5941
5942 /**
5943 * Check if add-on installed and activated on site.
5944 *
5945 * @author Vova Feldman (@svovaf)
5946 * @since 1.0.6
5947 *
5948 * @param string|number $id_or_slug
5949 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5950 *
5951 * @return bool
5952 */
5953 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5954 $this->_logger->entrance();
5955
5956 $addon_id = self::get_module_id( $id_or_slug );
5957 $is_activated = self::has_instance( $addon_id );
5958
5959 if ( ! $is_activated ) {
5960 return false;
5961 }
5962
5963 if ( is_bool( $is_premium ) ) {
5964 // Check if the specified code version is activate.
5965 $addon = $this->get_addon_instance( $addon_id );
5966 $is_activated = ( $is_premium === $addon->is_premium() );
5967 }
5968
5969 return $is_activated;
5970 }
5971
5972 /**
5973 * Check if add-on was connected to install
5974 *
5975 * @author Vova Feldman (@svovaf)
5976 * @since 1.1.7
5977 *
5978 * @param string|number $id_or_slug
5979 *
5980 * @return bool
5981 */
5982 function is_addon_connected( $id_or_slug ) {
5983 $this->_logger->entrance();
5984
5985 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5986
5987 $addon_id = self::get_module_id( $id_or_slug );
5988 $addon = $this->get_addon( $addon_id );
5989 $slug = $addon->slug;
5990 if ( ! isset( $sites[ $slug ] ) ) {
5991 return false;
5992 }
5993
5994 $site = $sites[ $slug ];
5995
5996 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5997
5998 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5999 // The given slug do NOT belong to any of the plugin's add-ons.
6000 return false;
6001 }
6002
6003 return ( is_object( $site ) &&
6004 is_numeric( $site->id ) &&
6005 is_numeric( $site->user_id ) &&
6006 FS_Plugin_Plan::is_valid_id( $site->plan_id )
6007 );
6008 }
6009
6010 /**
6011 * Determines if add-on installed.
6012 *
6013 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
6014 *
6015 * @author Vova Feldman (@svovaf)
6016 * @since 1.0.6
6017 *
6018 * @param string|number $id_or_slug
6019 *
6020 * @return bool
6021 */
6022 function is_addon_installed( $id_or_slug ) {
6023 $this->_logger->entrance();
6024
6025 $addon_id = self::get_module_id( $id_or_slug );
6026
6027 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
6028 }
6029
6030 /**
6031 * Get add-on basename.
6032 *
6033 * @author Vova Feldman (@svovaf)
6034 * @since 1.0.6
6035 *
6036 * @param string|number $id_or_slug
6037 *
6038 * @return string
6039 */
6040 function get_addon_basename( $id_or_slug ) {
6041 $addon_id = self::get_module_id( $id_or_slug );
6042
6043 if ( $this->is_addon_activated( $addon_id ) ) {
6044 return self::instance( $addon_id )->get_plugin_basename();
6045 }
6046
6047 $addon = $this->get_addon( $addon_id );
6048 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
6049
6050 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
6051 return $premium_basename;
6052 }
6053
6054 $all_plugins = $this->get_all_plugins();
6055
6056 foreach ( $all_plugins as $basename => $data ) {
6057 if ( $addon->slug === $data['slug'] ||
6058 $addon->premium_slug === $data['slug']
6059 ) {
6060 return $basename;
6061 }
6062 }
6063
6064 $free_basename = "{$addon->slug}/{$addon->slug}.php";
6065
6066 return $free_basename;
6067 }
6068
6069 /**
6070 * Get installed add-ons instances.
6071 *
6072 * @author Vova Feldman (@svovaf)
6073 * @since 1.0.6
6074 *
6075 * @return Freemius[]
6076 */
6077 function get_installed_addons() {
6078 if ( $this->is_addon() ) {
6079 // Add-on cannot have add-ons.
6080 return array();
6081 }
6082
6083 $installed_addons = array();
6084
6085 foreach ( self::$_instances as $instance ) {
6086 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
6087 $installed_addons[] = $instance;
6088 }
6089 }
6090
6091 return $installed_addons;
6092 }
6093
6094 /**
6095 * Check if any add-ons of the plugin are installed.
6096 *
6097 * @author Leo Fajardo (@leorw)
6098 * @since 1.1.1
6099 *
6100 * @return bool
6101 */
6102 function has_installed_addons() {
6103 if ( ! $this->has_addons() ) {
6104 return false;
6105 }
6106
6107 foreach ( self::$_instances as $instance ) {
6108 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
6109 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
6110 return true;
6111 }
6112 }
6113 }
6114
6115 return false;
6116 }
6117
6118 /**
6119 * Tell Freemius that the current plugin is an add-on.
6120 *
6121 * @author Vova Feldman (@svovaf)
6122 * @since 1.0.6
6123 *
6124 * @param number $parent_plugin_id The parent plugin ID
6125 */
6126 function init_addon( $parent_plugin_id ) {
6127 $this->_plugin->parent_plugin_id = $parent_plugin_id;
6128 }
6129
6130 /**
6131 * @author Vova Feldman (@svovaf)
6132 * @since 1.0.6
6133 *
6134 * @return bool
6135 */
6136 function is_addon() {
6137 return (
6138 isset( $this->_plugin->parent_plugin_id ) &&
6139 is_numeric( $this->_plugin->parent_plugin_id )
6140 );
6141 }
6142
6143 /**
6144 * @author Vova Feldman (@svovaf)
6145 * @since 2.3.2
6146 *
6147 * @param number $parent_product_id
6148 *
6149 * @return bool
6150 */
6151 function is_addon_of( $parent_product_id ) {
6152 return (
6153 $this->is_addon() &&
6154 $parent_product_id == $this->_plugin->parent_plugin_id
6155 );
6156 }
6157
6158 /**
6159 * Deactivate add-on if it's premium only and the user does't have a valid license.
6160 *
6161 * @param bool $is_after_trial_cancel
6162 *
6163 * @return bool If add-on was deactivated.
6164 */
6165 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6166 if ( ! $this->has_free_plan() &&
6167 ! $this->has_features_enabled_license() &&
6168 ! $this->_has_premium_license()
6169 ) {
6170 if ( $this->is_registered() ) {
6171 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6172 // if (empty($this->_storage->activation_timestamp) ||
6173 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6174 // ) {
6175 /**
6176 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6177 *
6178 * Retry syncing the user add-on licenses.
6179 */
6180 // Sync licenses.
6181 $this->_sync_licenses();
6182 // }
6183
6184 // Try to activate premium license.
6185 $this->_activate_license( true );
6186 }
6187
6188 if ( ! $this->has_free_plan() &&
6189 ! $this->has_features_enabled_license() &&
6190 ! $this->_has_premium_license()
6191 ) {
6192 // @todo Check if deactivate plugins also call the deactivation hook.
6193
6194 $this->_parent->_admin_notices->add_sticky(
6195 sprintf(
6196 ( $is_after_trial_cancel ?
6197 $this->_parent->get_text_inline(
6198 '%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.',
6199 'addon-trial-cancelled-message'
6200 ) :
6201 $this->_parent->get_text_inline(
6202 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6203 'addon-no-license-message'
6204 )
6205 ),
6206 '<b>' . $this->_plugin->title . '</b>'
6207 ) . ' ' . sprintf(
6208 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6209 $this->_parent->addon_url( $this->_slug ),
6210 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6211 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6212 ),
6213 'no_addon_license_' . $this->_slug,
6214 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6215 ( $is_after_trial_cancel ? 'success' : 'error' )
6216 );
6217
6218 deactivate_plugins( array( $this->_plugin_basename ), true );
6219
6220 return true;
6221 }
6222 }
6223
6224 return false;
6225 }
6226
6227 #endregion
6228
6229 #----------------------------------------------------------------------------------
6230 #region Sandbox
6231 #----------------------------------------------------------------------------------
6232
6233 /**
6234 * Set Freemius into sandbox mode for debugging.
6235 *
6236 * @author Vova Feldman (@svovaf)
6237 * @since 1.0.4
6238 *
6239 * @param string $secret_key
6240 */
6241 function init_sandbox( $secret_key ) {
6242 $this->_plugin->secret_key = $secret_key;
6243
6244 // Update plugin details.
6245 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6246 }
6247
6248 /**
6249 * Check if running payments in sandbox mode.
6250 *
6251 * @author Vova Feldman (@svovaf)
6252 * @since 1.0.4
6253 *
6254 * @return bool
6255 */
6256 function is_payments_sandbox() {
6257 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6258 }
6259
6260 #endregion
6261
6262 /**
6263 * Check if running test vs. live plugin.
6264 *
6265 * @author Vova Feldman (@svovaf)
6266 * @since 1.0.5
6267 *
6268 * @return bool
6269 */
6270 function is_live() {
6271 return $this->_plugin->is_live;
6272 }
6273
6274 /**
6275 * Check if super-admin skipped connection for all sites in the network.
6276 *
6277 * @author Vova Feldman (@svovaf)
6278 * @since 2.0.0
6279 */
6280 function is_network_anonymous() {
6281 if ( ! $this->_is_network_active ) {
6282 return false;
6283 }
6284
6285 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6286
6287 if ( empty( $is_anonymous_ms ) ) {
6288 return false;
6289 }
6290
6291 return $is_anonymous_ms['is'];
6292 }
6293
6294 /**
6295 * Check if super-admin opted-in for all sites in the network.
6296 *
6297 * @author Vova Feldman (@svovaf)
6298 * @since 2.0.0
6299 */
6300 function is_network_connected() {
6301 if ( ! $this->_is_network_active ) {
6302 return false;
6303 }
6304
6305 return $this->_storage->get( 'is_network_connected' );
6306 }
6307
6308 /**
6309 * Check if the user skipped connecting the account with Freemius.
6310 *
6311 * @author Vova Feldman (@svovaf)
6312 * @since 1.0.7
6313 *
6314 * @return bool
6315 */
6316 function is_anonymous() {
6317 if ( ! isset( $this->_is_anonymous ) ) {
6318 if ( $this->is_network_anonymous() ) {
6319 $this->_is_anonymous = true;
6320 } else if ( ! fs_is_network_admin() ) {
6321 if ( ! isset( $this->_storage->is_anonymous ) ) {
6322 // Not skipped.
6323 $this->_is_anonymous = false;
6324 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6325 // For back compatibility, since the variable was boolean before.
6326 $this->_is_anonymous = $this->_storage->is_anonymous;
6327
6328 // Upgrade stored data format to 1.1.3 format.
6329 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6330 } else {
6331 // Version 1.1.3 and later.
6332 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6333 }
6334 }
6335 }
6336
6337 return $this->_is_anonymous;
6338 }
6339
6340 /**
6341 * Check if the user skipped the connection of a specified site.
6342 *
6343 * @author Vova Feldman (@svovaf)
6344 * @since 2.0.0
6345 *
6346 * @param int $blog_id
6347 *
6348 * @return bool
6349 */
6350 function is_anonymous_site( $blog_id = 0 ) {
6351 if ( $this->is_network_anonymous() ) {
6352 return true;
6353 }
6354
6355 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6356
6357 if ( empty( $is_anonymous ) ) {
6358 return false;
6359 }
6360
6361 return $is_anonymous['is'];
6362 }
6363
6364 /**
6365 * Check if user connected his account and install pending email activation.
6366 *
6367 * @author Vova Feldman (@svovaf)
6368 * @since 1.0.7
6369 *
6370 * @return bool
6371 */
6372 function is_pending_activation() {
6373 return $this->_storage->get( 'is_pending_activation', false );
6374 }
6375
6376 /**
6377 * Check if plugin must be WordPress.org compliant.
6378 *
6379 * @since 1.0.7
6380 *
6381 * @return bool
6382 */
6383 function is_org_repo_compliant() {
6384 return $this->_is_org_compliant;
6385 }
6386
6387 #--------------------------------------------------------------------------------
6388 #region WP Cron Common
6389 #--------------------------------------------------------------------------------
6390
6391 /**
6392 * @author Vova Feldman (@svovaf)
6393 * @since 2.0.0
6394 *
6395 * @param string $name Cron name.
6396 *
6397 * @return object
6398 */
6399 private function get_cron_data( $name ) {
6400 $this->_logger->entrance( $name );
6401
6402 /**
6403 * @var object $cron_data
6404 */
6405 return $this->_storage->get( "{$name}_cron", null );
6406 }
6407
6408 /**
6409 * @author Vova Feldman (@svovaf)
6410 * @since 2.0.0
6411 *
6412 * @param string $name Cron name.
6413 */
6414 private function clear_cron_data( $name ) {
6415 $this->_logger->entrance( $name );
6416
6417 $this->_storage->remove( "{$name}_cron" );
6418 }
6419
6420 /**
6421 * @author Vova Feldman (@svovaf)
6422 * @since 2.0.0
6423 *
6424 * @param string $name Cron name.
6425 * @param int $cron_blog_id The cron executing blog ID.
6426 */
6427 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6428 $this->_logger->entrance( $name );
6429
6430 $this->_storage->store( "{$name}_cron", (object) array(
6431 'version' => $this->get_plugin_version(),
6432 'blog_id' => $cron_blog_id,
6433 'sdk_version' => $this->version,
6434 'timestamp' => WP_FS__SCRIPT_START_TIME,
6435 'on' => true,
6436 ) );
6437 }
6438
6439 /**
6440 * Get the cron's executing blog ID.
6441 *
6442 * @author Vova Feldman (@svovaf)
6443 * @since 2.0.0
6444 *
6445 * @param string $name Cron name.
6446 *
6447 * @return int
6448 */
6449 private function get_cron_blog_id( $name ) {
6450 $this->_logger->entrance( $name );
6451
6452 /**
6453 * @var object $cron_data
6454 */
6455 $cron_data = $this->get_cron_data( $name );
6456
6457 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6458 $cron_data->blog_id :
6459 0;
6460 }
6461
6462 /**
6463 * @author Vova Feldman (@svovaf)
6464 * @since 2.0.0
6465 *
6466 * @param string $name Cron name.
6467 *
6468 * @return bool
6469 */
6470 private function is_cron_on( $name ) {
6471 $this->_logger->entrance( $name );
6472
6473 /**
6474 * @var object $cron_data
6475 */
6476 $cron_data = $this->get_cron_data( $name );
6477
6478 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6479 }
6480
6481 /**
6482 * Unix timestamp for previous cron execution or false if never executed.
6483 *
6484 * @author Vova Feldman (@svovaf)
6485 * @since 2.0.0
6486 *
6487 * @param string $name Cron name.
6488 *
6489 * @return int|false
6490 */
6491 private function cron_last_execution( $name ) {
6492 $this->_logger->entrance( $name );
6493
6494 return $this->_storage->get( "{$name}_timestamp" );
6495 }
6496
6497 /**
6498 * Set cron execution time to now.
6499 *
6500 * @author Vova Feldman (@svovaf)
6501 * @since 2.0.0
6502 *
6503 * @param string $name Cron name.
6504 */
6505 private function set_cron_execution_timestamp( $name ) {
6506 $this->_logger->entrance( $name );
6507
6508 $this->_storage->store( "{$name}_timestamp", time() );
6509 }
6510
6511 /**
6512 * Sets the keepalive time to now.
6513 *
6514 * @author Leo Fajardo (@leorw)
6515 * @since 2.2.3
6516 *
6517 * @param bool|null $use_network_level_storage
6518 */
6519 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6520 $this->_logger->entrance();
6521
6522 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6523 }
6524
6525 /**
6526 * Check if cron was executed in the last $period of seconds.
6527 *
6528 * @author Vova Feldman (@svovaf)
6529 * @since 2.0.0
6530 *
6531 * @param string $name Cron name.
6532 * @param int $period In seconds
6533 *
6534 * @return bool
6535 */
6536 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6537 $this->_logger->entrance( $name );
6538
6539 $last_execution = $this->cron_last_execution( $name );
6540
6541 if ( ! is_numeric( $last_execution ) ) {
6542 return false;
6543 }
6544
6545 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6546 }
6547
6548 /**
6549 * WP Cron is executed on a site level. When running in a multisite network environment
6550 * with the network integration activated, for optimization reasons, we are consolidating
6551 * the installs data sync cron to be executed only from a single site.
6552 *
6553 * @author Vova Feldman (@svovaf)
6554 * @since 2.0.0
6555 *
6556 * @param int $except_blog_id Target any except the excluded blog ID.
6557 *
6558 * @return int
6559 */
6560 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6561 if ( ! is_multisite() ) {
6562 return 0;
6563 }
6564
6565 if ( $this->_is_network_active &&
6566 is_numeric( $this->_storage->network_install_blog_id ) &&
6567 $except_blog_id != $this->_storage->network_install_blog_id &&
6568 self::is_site_active( $this->_storage->network_install_blog_id )
6569 ) {
6570 // Try to run cron from the main network blog.
6571 $install = $this->get_install_by_blog_id( $this->_storage->network_install_blog_id );
6572
6573 if ( is_object( $install ) &&
6574 ( $this->is_premium() || $install->is_tracking_allowed() )
6575 ) {
6576 return $this->_storage->network_install_blog_id;
6577 }
6578 }
6579
6580 // Get first opted-in blog ID with active tracking.
6581 $installs = $this->get_blog_install_map();
6582 foreach ( $installs as $blog_id => $install ) {
6583 if ( $except_blog_id != $blog_id &&
6584 self::is_site_active( $blog_id ) &&
6585 ( $this->is_premium() || $install->is_tracking_allowed() )
6586 ) {
6587 return $blog_id;
6588 }
6589 }
6590
6591 return 0;
6592 }
6593
6594 /**
6595 * @author Vova Feldman (@svovaf)
6596 * @since 2.0.0
6597 *
6598 * @param string $name Cron name.
6599 * @param string $action_tag Callback action tag.
6600 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6601 */
6602 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6603 $this->_logger->entrance( $name );
6604
6605 if ( ! $this->is_cron_on( $name ) ) {
6606 return;
6607 }
6608
6609 $clear_cron = true;
6610 if ( ! $is_network_clear && $this->_is_network_active ) {
6611 $installs = $this->get_blog_install_map();
6612
6613 foreach ( $installs as $blog_id => $install ) {
6614 /**
6615 * @var FS_Site $install
6616 */
6617 if ( $install->is_tracking_allowed() ) {
6618 $clear_cron = false;
6619 break;
6620 }
6621 }
6622 }
6623
6624 if ( ! $clear_cron ) {
6625 return;
6626 }
6627
6628 /**
6629 * @var object $cron_data
6630 */
6631 $cron_data = $this->get_cron_data( $name );
6632
6633 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6634 $cron_data->blog_id :
6635 0;
6636
6637 $this->clear_cron_data( $name );
6638
6639 if ( 0 < $cron_blog_id ) {
6640 switch_to_blog( $cron_blog_id );
6641 }
6642
6643 if ( empty( $action_tag ) ) {
6644 $action_tag = $name;
6645 }
6646
6647 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6648
6649 if ( 0 < $cron_blog_id ) {
6650 restore_current_blog();
6651 }
6652 }
6653
6654 /**
6655 * Unix timestamp for next cron execution or false if not scheduled.
6656 *
6657 * @author Vova Feldman (@svovaf)
6658 * @since 2.0.0
6659 *
6660 * @param string $name Cron name.
6661 * @param string $action_tag Callback action tag.
6662 *
6663 * @return int|false
6664 */
6665 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6666 $this->_logger->entrance( $name );
6667
6668 if ( ! $this->is_cron_on( $name ) ) {
6669 return false;
6670 }
6671
6672 /**
6673 * @var object $cron_data
6674 */
6675 $cron_data = $this->get_cron_data( $name );
6676
6677 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6678 $cron_data->blog_id :
6679 0;
6680
6681 if ( 0 < $cron_blog_id ) {
6682 switch_to_blog( $cron_blog_id );
6683 }
6684
6685 if ( empty( $action_tag ) ) {
6686 $action_tag = $name;
6687 }
6688
6689 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6690
6691 if ( 0 < $cron_blog_id ) {
6692 restore_current_blog();
6693 }
6694
6695 return $next_scheduled;
6696 }
6697
6698 /**
6699 * @author Vova Feldman (@svovaf)
6700 * @since 2.0.0
6701 *
6702 * @param string $name Cron name.
6703 * @param string $action_tag Callback action tag.
6704 * @param string $recurrence 'single' or 'daily'.
6705 * @param int $start_at Defaults to now.
6706 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6707 * @param int $except_blog_id Target any except the excluded blog ID.
6708 */
6709 private function schedule_cron(
6710 $name,
6711 $action_tag = '',
6712 $recurrence = 'single',
6713 $start_at = WP_FS__SCRIPT_START_TIME,
6714 $randomize_start = true,
6715 $except_blog_id = 0
6716 ) {
6717 $this->_logger->entrance( $name );
6718
6719 $this->clear_cron( $name, $action_tag, true );
6720
6721 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6722
6723 if ( is_multisite() && 0 == $cron_blog_id ) {
6724 // Don't schedule cron since couldn't find a target blog.
6725 return;
6726 }
6727
6728 if ( 0 < $cron_blog_id ) {
6729 switch_to_blog( $cron_blog_id );
6730 }
6731
6732 if ( 'daily' === $recurrence ) {
6733 if ( $randomize_start ) {
6734 // Schedule first sync with a random 12 hour time range from now.
6735 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6736 }
6737
6738 // Schedule daily WP cron.
6739 wp_schedule_event(
6740 $start_at,
6741 'daily',
6742 $this->get_action_tag( $action_tag )
6743 );
6744 } else if ( 'single' === $recurrence ) {
6745 // Schedule single cron.
6746 wp_schedule_single_event(
6747 $start_at,
6748 $this->get_action_tag( $action_tag )
6749 );
6750 }
6751
6752 $this->set_cron_data( $name, $cron_blog_id );
6753
6754 if ( 0 < $cron_blog_id ) {
6755 restore_current_blog();
6756 }
6757 }
6758
6759 /**
6760 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6761 * that doesn't halt page loading.
6762 *
6763 * @author Vova Feldman (@svovaf)
6764 * @since 2.0.0
6765 *
6766 * @param string $name Cron name.
6767 * @param callable $callable The function that should be executed.
6768 */
6769 private function execute_cron( $name, $callable ) {
6770 $this->_logger->entrance( $name );
6771
6772 // Store the last time data sync was executed.
6773 $this->set_cron_execution_timestamp( $name );
6774
6775 // Check if API is temporary down.
6776 if ( FS_Api::is_temporary_down() ) {
6777 return;
6778 }
6779
6780 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6781
6782 $users_2_blog_ids = array();
6783
6784 if ( ! is_multisite() ) {
6785 // Add dummy blog.
6786 $users_2_blog_ids[0] = array( 0 );
6787 } else {
6788 $installs = $this->get_blog_install_map();
6789 foreach ( $installs as $blog_id => $install ) {
6790 if ( $this->is_premium() || $install->is_tracking_allowed() ) {
6791 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6792 $users_2_blog_ids[ $install->user_id ] = array();
6793 }
6794
6795 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6796 }
6797 }
6798 }
6799
6800 $current_blog_id = get_current_blog_id();
6801
6802 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6803 if ( 0 < $blog_ids[0] ) {
6804 $this->switch_to_blog( $blog_ids[0] );
6805 }
6806
6807 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6808
6809 foreach ( $blog_ids as $blog_id ) {
6810 $this->do_action( "after_{$name}_cron", $blog_id );
6811 }
6812 }
6813
6814 if ( is_multisite() ) {
6815 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6816
6817 $this->do_action( "after_{$name}_cron_multisite" );
6818 }
6819 }
6820
6821 #endregion
6822
6823 #----------------------------------------------------------------------------------
6824 #region Daily Sync Cron
6825 #----------------------------------------------------------------------------------
6826
6827
6828 /**
6829 * @author Vova Feldman (@svovaf)
6830 * @since 2.0.0
6831 *
6832 * @return bool
6833 */
6834 private function is_sync_cron_scheduled() {
6835 return $this->is_cron_on( 'sync' );
6836 }
6837
6838 /**
6839 * Get the sync cron's executing blog ID.
6840 *
6841 * @author Vova Feldman (@svovaf)
6842 * @since 2.0.0
6843 *
6844 * @return int
6845 */
6846 private function get_sync_cron_blog_id() {
6847 return $this->get_cron_blog_id( 'sync' );
6848 }
6849
6850 /**
6851 * @author Vova Feldman (@svovaf)
6852 * @since 1.1.7.3
6853 */
6854 private function run_manual_sync() {
6855 self::require_pluggable_essentials();
6856
6857 if ( ! $this->is_user_admin() ) {
6858 return;
6859 }
6860
6861 // Run manual sync.
6862 $this->_sync_cron();
6863
6864 // Reschedule next cron to run 24 hours from now (performance optimization).
6865 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6866 }
6867
6868 /**
6869 * Data sync cron job. Replaces the background sync non blocking HTTP request
6870 * that doesn't halt page loading.
6871 *
6872 * @author Vova Feldman (@svovaf)
6873 * @since 1.1.7.3
6874 * @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.
6875 */
6876 function _sync_cron() {
6877 $this->_logger->entrance();
6878
6879 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6880 }
6881
6882 /**
6883 * The actual data sync cron logic.
6884 *
6885 * @author Vova Feldman (@svovaf)
6886 * @since 2.0.0
6887 *
6888 * @param int[] $blog_ids
6889 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6890 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6891 * updates for a single site in case `execute_cron` has switched to a different blog.
6892 */
6893 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6894 if ( $this->is_registered() ) {
6895 if ( $this->has_paid_plan() ) {
6896 // Initiate background plan sync.
6897 $this->_sync_license( true, false, $current_blog_id );
6898
6899 if ( $this->is_paying() ) {
6900 // Check for premium plugin updates.
6901 $this->check_updates( true );
6902 }
6903 } else {
6904 // Sync install(s) (only if something changed locally).
6905 if ( 1 < count( $blog_ids ) ) {
6906 $this->sync_installs();
6907 } else {
6908 $this->sync_install();
6909 }
6910
6911 $this->maybe_sync_install_user();
6912 }
6913 }
6914 }
6915
6916 /**
6917 * Check if sync was executed in the last $period of seconds.
6918 *
6919 * @author Vova Feldman (@svovaf)
6920 * @since 1.1.7.3
6921 *
6922 * @param int $period In seconds
6923 *
6924 * @return bool
6925 */
6926 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6927 return $this->is_cron_executed( 'sync', $period );
6928 }
6929
6930 /**
6931 * @author Vova Feldman (@svovaf)
6932 * @since 1.1.7.3
6933 *
6934 * @return bool
6935 */
6936 private function is_sync_cron_on() {
6937 return $this->is_cron_on( 'sync' );
6938 }
6939
6940 /**
6941 * @author Vova Feldman (@svovaf)
6942 * @since 1.1.7.3
6943 *
6944 * @param int $start_at Defaults to now.
6945 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6946 * @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.
6947 */
6948 private function schedule_sync_cron(
6949 $start_at = WP_FS__SCRIPT_START_TIME,
6950 $randomize_start = true,
6951 $except_blog_id = 0
6952 ) {
6953 $this->schedule_cron(
6954 'sync',
6955 'data_sync',
6956 'daily',
6957 $start_at,
6958 $randomize_start,
6959 $except_blog_id
6960 );
6961 }
6962
6963 /**
6964 * Add the actual sync function to the cron job hook.
6965 *
6966 * @author Vova Feldman (@svovaf)
6967 * @since 1.1.7.3
6968 */
6969 private function hook_callback_to_sync_cron() {
6970 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6971 }
6972
6973 /**
6974 * @author Vova Feldman (@svovaf)
6975 * @since 1.1.7.3
6976 *
6977 * @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.
6978 */
6979 private function clear_sync_cron( $is_network_clear = false ) {
6980 $this->_logger->entrance();
6981
6982 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6983 }
6984
6985 /**
6986 * Unix timestamp for next sync cron execution or false if not scheduled.
6987 *
6988 * @author Vova Feldman (@svovaf)
6989 * @since 1.1.7.3
6990 *
6991 * @return int|false
6992 */
6993 function next_sync_cron() {
6994 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6995 }
6996
6997 /**
6998 * Unix timestamp for previous sync cron execution or false if never executed.
6999 *
7000 * @author Vova Feldman (@svovaf)
7001 * @since 1.1.7.3
7002 *
7003 * @return int|false
7004 */
7005 function last_sync_cron() {
7006 return $this->cron_last_execution( 'sync' );
7007 }
7008
7009 #endregion Daily Sync Cron ------------------------------------------------------------------
7010
7011 #----------------------------------------------------------------------------------
7012 #region Async Install Sync
7013 #----------------------------------------------------------------------------------
7014
7015 /**
7016 * @author Vova Feldman (@svovaf)
7017 * @since 1.1.7.3
7018 *
7019 * @return bool
7020 */
7021 private function is_install_sync_scheduled() {
7022 return $this->is_cron_on( 'install_sync' );
7023 }
7024
7025 /**
7026 * Get the sync cron's executing blog ID.
7027 *
7028 * @author Vova Feldman (@svovaf)
7029 * @since 2.0.0
7030 *
7031 * @return int
7032 */
7033 private function get_install_sync_cron_blog_id() {
7034 return $this->get_cron_blog_id( 'install_sync' );
7035 }
7036
7037 /**
7038 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
7039 *
7040 * @author Vova Feldman (@svovaf)
7041 * @since 1.1.7.3
7042 *
7043 * @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.
7044 */
7045 private function schedule_install_sync( $except_blog_id = 0 ) {
7046 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
7047 }
7048
7049 /**
7050 * Unix timestamp for previous install sync cron execution or false if never executed.
7051 *
7052 * @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.
7053 *
7054 * @author Vova Feldman (@svovaf)
7055 * @since 1.1.7.3
7056 *
7057 * @return int|false
7058 */
7059 function last_install_sync() {
7060 return $this->cron_last_execution( 'install_sync' );
7061 }
7062
7063 /**
7064 * Unix timestamp for next install sync cron execution or false if not scheduled.
7065 *
7066 * @author Vova Feldman (@svovaf)
7067 * @since 1.1.7.3
7068 *
7069 * @return int|false
7070 */
7071 function next_install_sync() {
7072 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
7073 }
7074
7075 /**
7076 * Add the actual install sync function to the cron job hook.
7077 *
7078 * @author Vova Feldman (@svovaf)
7079 * @since 1.1.7.3
7080 */
7081 private function hook_callback_to_install_sync() {
7082 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
7083 }
7084
7085 /**
7086 * @author Vova Feldman (@svovaf)
7087 * @since 1.1.7.3
7088 *
7089 * @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.
7090 */
7091 private function clear_install_sync_cron( $is_network_clear = false ) {
7092 $this->_logger->entrance();
7093
7094 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
7095 }
7096
7097 /**
7098 * @author Vova Feldman (@svovaf)
7099 * @since 1.1.7.3
7100 * @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.
7101 */
7102 public function _run_sync_install() {
7103 $this->_logger->entrance();
7104
7105 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
7106 }
7107
7108 /**
7109 * The actual install(s) sync cron logic.
7110 *
7111 * @author Vova Feldman (@svovaf)
7112 * @since 2.0.0
7113 *
7114 * @param int[] $blog_ids
7115 * @param int|null $current_blog_id
7116 */
7117 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
7118 if ( $this->is_registered() ) {
7119 if ( 1 < count( $blog_ids ) ) {
7120 $this->sync_installs( array(), true );
7121 } else {
7122 $this->sync_install( array(), true );
7123 }
7124
7125 $this->maybe_sync_install_user();
7126 }
7127 }
7128
7129 #endregion Async Install Sync ------------------------------------------------------------------
7130
7131 /**
7132 * Show a notice that activation is currently pending.
7133 *
7134 * @author Vova Feldman (@svovaf)
7135 * @since 1.0.7
7136 *
7137 * @param bool|string $email
7138 * @param bool $is_pending_trial Since 1.2.1.5
7139 */
7140 function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) {
7141 if ( ! is_string( $email ) ) {
7142 $current_user = self::_get_current_wp_user();
7143 $email = $current_user->user_email;
7144 }
7145
7146 $this->_admin_notices->add_sticky(
7147 sprintf(
7148 $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' ),
7149 '<b>' . $this->get_plugin_name() . '</b>',
7150 '<b>' . $email . '</b>',
7151 ( $is_pending_trial ?
7152 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7153 $this->get_text_inline( 'complete the install', 'complete-the-install' ) )
7154 ),
7155 'activation_pending',
7156 'Thanks!'
7157 );
7158 }
7159
7160 /**
7161 * Check if currently in plugin activation.
7162 *
7163 * @author Vova Feldman (@svovaf)
7164 * @since 1.1.4
7165 *
7166 * @return bool
7167 */
7168 function is_plugin_activation() {
7169 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7170
7171 return !empty($result);
7172 }
7173
7174 /**
7175 *
7176 * NOTE: admin_menu action executed before admin_init.
7177 *
7178 * @author Vova Feldman (@svovaf)
7179 * @since 1.0.7
7180 */
7181 function _admin_init_action() {
7182 $is_migration = $this->is_migration();
7183
7184 /**
7185 * Automatically redirect to connect/activation page after plugin activation.
7186 *
7187 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7188 */
7189 if ( $this->is_plugin_activation() ) {
7190 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7191
7192 if ( isset( $_GET['activate-multi'] ) ) {
7193 /**
7194 * Don't redirect if activating multiple plugins at once (bulk activation).
7195 */
7196 } else if ( ! $is_migration ) {
7197 $this->_redirect_on_activation_hook();
7198 return;
7199 }
7200 }
7201
7202 if ( $is_migration ) {
7203 return;
7204 }
7205
7206 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7207 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7208
7209 $this->skip_connection( null, fs_is_network_admin() );
7210
7211 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7212 }
7213
7214 if ( $this->is_network_activation_mode() &&
7215 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7216 ) {
7217 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7218
7219 $this->delegate_connection();
7220
7221 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7222 }
7223
7224 $this->_add_upgrade_action_link();
7225
7226 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7227 (
7228 ( true === $this->_storage->require_license_activation ) ||
7229 // Not registered nor anonymous.
7230 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7231 // OR, network level and in network upgrade mode.
7232 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7233 )
7234 ) {
7235 if ( ! $this->is_pending_activation() ) {
7236 if ( ! $this->is_activation_page() ) {
7237 /**
7238 * If a user visits any other admin page before activating the premium-only theme with a valid
7239 * license, reactivate the previous theme.
7240 *
7241 * @author Leo Fajardo (@leorw)
7242 * @since 1.2.2
7243 */
7244 if ( $this->is_theme() &&
7245 ! $this->has_settings_menu() &&
7246 ! isset( $_REQUEST['fs_action'] ) &&
7247 $this->can_activate_previous_theme()
7248 ) {
7249 if ( $this->is_only_premium() ) {
7250 $this->activate_previous_theme();
7251 return;
7252 }
7253
7254 if ( true === $this->_storage->require_license_activation ) {
7255 $this->_storage->require_license_activation = false;
7256 }
7257 }
7258
7259 if ( ! fs_is_network_admin() &&
7260 $this->is_network_activation_mode() &&
7261 ! $this->is_delegated_connection()
7262 ) {
7263 return;
7264 }
7265
7266 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7267 if ( ! $this->_anonymous_mode &&
7268 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7269 // Show notice for new plugin installations.
7270 $this->_admin_notices->add(
7271 sprintf(
7272 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7273 sprintf( '<b><a href="%s">%s</a></b>',
7274 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7275 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7276 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7277 )
7278 ),
7279 '',
7280 'update-nag'
7281 );
7282 }
7283 } else {
7284 if ( $this->should_add_sticky_optin_notice() ) {
7285 $this->add_sticky_optin_admin_notice();
7286 }
7287
7288 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7289 // Don't show admin nag if plugin update.
7290 wp_enqueue_script( 'wp-pointer' );
7291 wp_enqueue_style( 'wp-pointer' );
7292
7293 $this->_enqueue_connect_essentials();
7294
7295 add_action( 'admin_print_footer_scripts', array(
7296 $this,
7297 '_add_connect_pointer_script'
7298 ) );
7299 }
7300 }
7301 }
7302 }
7303
7304 if ( $this->show_opt_in_on_themes_page() &&
7305 $this->is_activation_page()
7306 ) {
7307 $this->_show_theme_activation_optin_dialog();
7308 }
7309 }
7310 }
7311
7312 /**
7313 * @author Vova Feldman (@svovaf)
7314 * @since 2.0.0
7315 *
7316 * @return bool
7317 */
7318 private function should_add_sticky_optin_notice() {
7319 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7320 return false;
7321 }
7322
7323 if ( fs_is_network_admin() ) {
7324 if ( ! $this->_is_network_active ) {
7325 return false;
7326 }
7327
7328 if ( ! $this->is_network_activation_mode() ) {
7329 return false;
7330 }
7331
7332 return ! isset( $this->_storage->sticky_optin_added_ms );
7333 }
7334
7335 if ( ! $this->is_activation_mode() ) {
7336 return false;
7337 }
7338
7339 // If running from a blog admin and delegated the connection.
7340 return ! isset( $this->_storage->sticky_optin_added );
7341 }
7342
7343 /**
7344 * @author Leo Fajardo (@leorw)
7345 * @since 2.0.0
7346 */
7347 private function add_sticky_optin_admin_notice() {
7348 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7349 $this->_storage->sticky_optin_added = true;
7350 } else {
7351 $this->_storage->sticky_optin_added_ms = true;
7352 }
7353
7354 // Show notice for new plugin installations.
7355 $this->_admin_notices->add_sticky(
7356 sprintf(
7357 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7358 $this->_module_type,
7359 sprintf( '<b><a href="%s">%s</a></b>',
7360 $this->get_activation_url(),
7361 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7362 )
7363 ),
7364 'connect_account',
7365 '',
7366 'update-nag'
7367 );
7368 }
7369
7370 /**
7371 * Enqueue connect requires scripts and styles.
7372 *
7373 * @author Vova Feldman (@svovaf)
7374 * @since 1.1.4
7375 */
7376 function _enqueue_connect_essentials() {
7377 wp_enqueue_script( 'jquery' );
7378 wp_enqueue_script( 'json2' );
7379
7380 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7381 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7382
7383 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
7384 }
7385
7386 /**
7387 * Add connect / opt-in pointer.
7388 *
7389 * @author Vova Feldman (@svovaf)
7390 * @since 1.1.4
7391 */
7392 function _add_connect_pointer_script() {
7393 $vars = array( 'id' => $this->_module_id );
7394 $pointer_content = fs_get_template( 'connect.php', $vars );
7395 ?>
7396 <script type="text/javascript">// <![CDATA[
7397 jQuery(document).ready(function ($) {
7398 if ('undefined' !== typeof(jQuery().pointer)) {
7399
7400 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7401
7402 if (element.length > 0) {
7403 var optin = $(element).pointer($.extend(true, {}, {
7404 content : <?php echo json_encode( $pointer_content ) ?>,
7405 position : {
7406 edge : 'left',
7407 align: 'center'
7408 },
7409 buttons : function () {
7410 // Don't show pointer buttons.
7411 return '';
7412 },
7413 pointerWidth: 482
7414 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7415
7416 <?php
7417 echo $this->apply_filters( 'optin_pointer_execute', "
7418
7419 optin.pointer('open');
7420
7421 // Tag the opt-in pointer with custom class.
7422 $('.wp-pointer #fs_connect')
7423 .parents('.wp-pointer.wp-pointer-top')
7424 .addClass('fs-opt-in-pointer');
7425
7426 ", 'element', 'optin' ) ?>
7427 }
7428 }
7429 });
7430 // ]]></script>
7431 <?php
7432 }
7433
7434 /**
7435 * Return current page's URL.
7436 *
7437 * @author Vova Feldman (@svovaf)
7438 * @since 1.0.7
7439 *
7440 * @return string
7441 */
7442 function current_page_url() {
7443 $url = 'http';
7444
7445 if ( isset( $_SERVER["HTTPS"] ) ) {
7446 if ( $_SERVER["HTTPS"] == "on" ) {
7447 $url .= "s";
7448 }
7449 }
7450 $url .= "://";
7451 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7452 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7453 } else {
7454 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7455 }
7456
7457 return esc_url( $url );
7458 }
7459
7460 /**
7461 * Check if the current page is the plugin's main admin settings page.
7462 *
7463 * @author Vova Feldman (@svovaf)
7464 * @since 1.0.7
7465 *
7466 * @return bool
7467 */
7468 function _is_plugin_page() {
7469 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7470 fs_is_plugin_page( $this->_slug );
7471 }
7472
7473 /* Events
7474 ------------------------------------------------------------------------------------------------------------------*/
7475 /**
7476 * Delete site install from Database.
7477 *
7478 * @author Vova Feldman (@svovaf)
7479 * @since 1.0.1
7480 *
7481 * @param bool $store
7482 * @param int|null $blog_id Since 2.0.0
7483 *
7484 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7485 */
7486 function _delete_site( $store = true, $blog_id = null ) {
7487 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7488 }
7489
7490 /**
7491 * Delete site install from Database.
7492 *
7493 * @author Vova Feldman (@svovaf)
7494 * @since 1.2.2.7
7495 *
7496 * @param string $slug
7497 * @param string $module_type
7498 * @param bool $store
7499 * @param int|null $blog_id Since 2.0.0
7500 *
7501 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7502 */
7503 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7504 $sites = self::get_all_sites( $module_type, $blog_id );
7505
7506 $install_id = false;
7507
7508 if ( isset( $sites[ $slug ] ) ) {
7509 if ( is_object( $sites[ $slug ] ) ) {
7510 $install_id = $sites[ $slug ]->id;
7511 }
7512
7513 unset( $sites[ $slug ] );
7514
7515 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7516 }
7517
7518 return $install_id;
7519 }
7520
7521 /**
7522 * Delete user.
7523 *
7524 * @author Vova Feldman (@svovaf)
7525 * @since 2.0.0
7526 *
7527 * @param number $user_id
7528 * @param bool $store
7529 *
7530 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7531 */
7532 private static function delete_user( $user_id, $store = true ) {
7533 $users = self::get_all_users();
7534
7535 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7536 return false;
7537 }
7538
7539 unset( $users[ $user_id ] );
7540
7541 self::$_accounts->set_option( 'users', $users, $store );
7542
7543 return $user_id;
7544 }
7545
7546 /**
7547 * Delete plugin's plans information.
7548 *
7549 * @param bool $store Flush to Database if true.
7550 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7551 *
7552 * @author Vova Feldman (@svovaf)
7553 * @since 1.0.9
7554 */
7555 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7556 $this->_logger->entrance();
7557
7558 $plans = self::get_all_plans( $this->_module_type );
7559
7560 $plans_to_keep = array();
7561
7562 if ( $keep_associated_plans ) {
7563 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7564 foreach ( $plans_ids_to_keep as $plan_id ) {
7565 $plan = self::_get_plan_by_id( $plan_id );
7566 if ( is_object( $plan ) ) {
7567 $plans_to_keep[] = self::_encrypt_entity( $plan );
7568 }
7569 }
7570 }
7571
7572 if ( ! empty( $plans_to_keep ) ) {
7573 $plans[ $this->_slug ] = $plans_to_keep;
7574 } else {
7575 unset( $plans[ $this->_slug ] );
7576 }
7577
7578 $this->set_account_option( 'plans', $plans, $store );
7579 }
7580
7581 /**
7582 * Delete all plugin licenses.
7583 *
7584 * @author Vova Feldman (@svovaf)
7585 * @since 1.0.9
7586 *
7587 * @param bool $store
7588 */
7589 private function _delete_licenses( $store = true ) {
7590 $this->_logger->entrance();
7591
7592 $all_licenses = self::get_all_licenses();
7593
7594 unset( $all_licenses[ $this->_module_id ] );
7595
7596 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7597 }
7598
7599 /**
7600 * Check if Freemius was added on new plugin installation.
7601 *
7602 * @author Vova Feldman (@svovaf)
7603 * @since 1.1.5
7604 *
7605 * @return bool
7606 */
7607 function is_plugin_new_install() {
7608 return isset( $this->_storage->is_plugin_new_install ) &&
7609 $this->_storage->is_plugin_new_install;
7610 }
7611
7612 /**
7613 * Check if it's the first plugin release that is running Freemius.
7614 *
7615 * @author Vova Feldman (@svovaf)
7616 * @since 1.2.1.5
7617 *
7618 * @return bool
7619 */
7620 function is_first_freemius_powered_version() {
7621 return empty( $this->_storage->plugin_last_version );
7622 }
7623
7624 /**
7625 * @author Leo Fajardo (@leorw)
7626 * @since 1.2.2
7627 *
7628 * @return bool|string
7629 */
7630 private function get_previous_theme_slug() {
7631 return isset( $this->_storage->previous_theme ) ?
7632 $this->_storage->previous_theme :
7633 false;
7634 }
7635
7636 /**
7637 * @author Leo Fajardo (@leorw)
7638 * @since 1.2.2
7639 *
7640 * @return string
7641 */
7642 private function can_activate_previous_theme() {
7643 $slug = $this->get_previous_theme_slug();
7644 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7645 $theme_instance = wp_get_theme( $slug );
7646
7647 return $theme_instance->exists();
7648 }
7649
7650 return false;
7651 }
7652
7653 /**
7654 * @author Leo Fajardo (@leorw)
7655 * @since 1.2.2
7656 */
7657 private function activate_previous_theme() {
7658 switch_theme( $this->get_previous_theme_slug() );
7659 unset( $this->_storage->previous_theme );
7660
7661 global $pagenow;
7662 if ( 'themes.php' === $pagenow ) {
7663 /**
7664 * Refresh the active theme information.
7665 *
7666 * @author Leo Fajardo (@leorw)
7667 * @since 1.2.2
7668 */
7669 fs_redirect( $this->admin_url( $pagenow ) );
7670 }
7671 }
7672
7673 /**
7674 * @author Leo Fajardo (@leorw)
7675 * @since 1.2.2
7676 *
7677 * @return string
7678 */
7679 function get_previous_theme_activation_url() {
7680 if ( ! $this->can_activate_previous_theme() ) {
7681 return '';
7682 }
7683
7684 /**
7685 * Activation URL
7686 *
7687 * @author Leo Fajardo (@leorw)
7688 * @since 1.2.2
7689 */
7690 return wp_nonce_url(
7691 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7692 'switch-theme_' . $this->get_previous_theme_slug()
7693 );
7694 }
7695
7696 /**
7697 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7698 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7699 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7700 * theme doesn't exist, then there will be no close button.
7701 *
7702 * @author Leo Fajardo (@leorw)
7703 * @since 1.2.2
7704 *
7705 * @param string $slug_or_name Old theme's slug or name.
7706 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7707 */
7708 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7709 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7710 $old_theme->get_stylesheet() :
7711 $slug_or_name;
7712
7713 $this->_activate_plugin_event_hook();
7714 }
7715
7716 /**
7717 * Plugin activated hook.
7718 *
7719 * @author Vova Feldman (@svovaf)
7720 * @since 1.0.1
7721 *
7722 * @uses FS_Api
7723 */
7724 function _activate_plugin_event_hook() {
7725 $this->_logger->entrance( 'slug = ' . $this->_slug );
7726
7727 if ( ! $this->is_user_admin() ) {
7728 return;
7729 }
7730
7731 $this->unregister_uninstall_hook();
7732
7733 // Clear API cache on activation.
7734 FS_Api::clear_cache();
7735
7736 $is_premium_version_activation = $this->is_plugin() ?
7737 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7738 $this->is_premium();
7739
7740 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7741
7742 if ( $this->is_plugin() ) {
7743 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7744 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7745 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7746 $other_version_basename = $is_premium_version_activation ?
7747 $this->_free_plugin_basename :
7748 $this->premium_plugin_basename();
7749
7750 if ( ! $this->_is_network_active ) {
7751 /**
7752 * Themes are always network activated, but the ACTUAL activation is per site.
7753 *
7754 * During the activation, the plugin isn't yet active, therefore,
7755 * _is_network_active will be set to false even if it's a network level
7756 * activation. So we need to fix that by looking at the is_network_admin() value.
7757 *
7758 * @author Vova Feldman
7759 */
7760 $this->_is_network_active = (
7761 $this->_is_multisite_integrated &&
7762 fs_is_network_admin()
7763 );
7764 }
7765
7766 /**
7767 * If the other module version is active, deactivate it.
7768 *
7769 * is_plugin_active() checks if the plugin is active on the site or the network level and
7770 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7771 *
7772 * @author Leo Fajardo (@leorw)
7773 * @since 1.2.2
7774 */
7775 if (
7776 is_plugin_active( $other_version_basename ) &&
7777 $this->apply_filters( 'deactivate_on_activation', true )
7778 ) {
7779 deactivate_plugins( $other_version_basename );
7780 }
7781 }
7782
7783 if ( $this->is_registered() ) {
7784 if ( $is_premium_version_activation ) {
7785 $this->reconnect_locally();
7786 }
7787
7788
7789 // Schedule re-activation event and sync.
7790 // $this->sync_install( array(), true );
7791 $this->schedule_install_sync();
7792
7793 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7794 if ( $is_premium_version_activation ) {
7795 $this->_admin_notices->add(
7796 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7797 $this->get_text_x_inline( 'W00t',
7798 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7799 );
7800 }
7801 } else if ( $this->is_anonymous() ) {
7802 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7803 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7804 $network = true;
7805 } else {
7806 $plugin_version = $this->_storage->is_anonymous['version'];
7807 $network = false;
7808 }
7809
7810 /**
7811 * Reset "skipped" click cache on the following:
7812 * 1. Freemius DEV mode.
7813 * 2. WordPress DEBUG mode.
7814 * 3. If a plugin and the user skipped the exact same version before.
7815 *
7816 * @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).
7817 *
7818 * @todo 4. If explicitly asked to retry after every activation.
7819 */
7820 if ( WP_FS__DEV_MODE ||
7821 (
7822 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7823 $this->get_plugin_version() == $plugin_version
7824 )
7825 ) {
7826 $this->reset_anonymous_mode( $network );
7827 }
7828 }
7829
7830 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7831
7832 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7833 /**
7834 * When activating an add-on, try to also activate a license.
7835 *
7836 * @author Leo Fajardo (@leorw)
7837 * @since 2.3.0
7838 */
7839 if ( ! $this->_is_network_active ) {
7840 $this->maybe_activate_addon_license();
7841 } else {
7842 $this->maybe_network_activate_addon_license();
7843 }
7844
7845 /**
7846 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7847 *
7848 * @author Leo Fajardo (@leorw)
7849 * @since 2.3.0
7850 */
7851 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7852
7853 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7854 $this->_storage->require_license_activation = false;
7855 }
7856 }
7857
7858 if (
7859 $is_premium_version_activation &&
7860 (
7861 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7862 (
7863 $this->is_registered() &&
7864 ! $is_trial_or_has_features_enabled_license
7865 )
7866 )
7867 ) {
7868 $this->_storage->require_license_activation = true;
7869 }
7870
7871 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7872 /**
7873 * If no previous version of plugin's version exist, it means that it's either
7874 * the first time that the plugin installed on the site, or the plugin was installed
7875 * before but didn't have Freemius integrated.
7876 *
7877 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7878 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7879 * only after immediate activation.
7880 *
7881 * @since 1.1.4
7882 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7883 */
7884 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7885 }
7886
7887 /**
7888 * Also flush when activating the premium version so that even if Freemius was off before, the API
7889 * connectivity test can be run again.
7890 *
7891 * @author Leo Fajardo (@leorw)
7892 * @since 2.2.3.1
7893 */
7894 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7895
7896 if ( ! $this->_anonymous_mode &&
7897 $has_api_connectivity &&
7898 ! $this->_isAutoInstall
7899 ) {
7900 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7901 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7902 }
7903
7904 /**
7905 * Activation hook is executed after the plugin's main file is loaded, therefore,
7906 * after the plugin was loaded. The logic is located at activate_plugin()
7907 * ./wp-admin/includes/plugin.php.
7908 *
7909 * @author Vova Feldman (@svovaf)
7910 * @since 1.1.9
7911 */
7912 $this->_storage->was_plugin_loaded = true;
7913 }
7914
7915 /**
7916 * @author Leo Fajardo (@leorw)
7917 * @since 2.3.0
7918 */
7919 private function maybe_activate_addon_license() {
7920 $parent_fs = $this->get_parent_instance();
7921
7922 if (
7923 ! is_object( $parent_fs ) ||
7924 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7925 ) {
7926 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7927 return;
7928 }
7929
7930 $license = $this->get_active_parent_license();
7931 if ( ! is_object( $license ) ) {
7932 return;
7933 }
7934
7935 if (
7936 $this->is_bundle_license_auto_activation_enabled() &&
7937 ! empty( $license->products )
7938 ) {
7939 $this->activate_bundle_license( $license );
7940
7941 return;
7942 }
7943
7944 if ( ! $this->is_registered() ) {
7945 // Opt in with a license key.
7946 $this->opt_in(
7947 $parent_fs->get_current_or_network_user()->email,
7948 false,
7949 false,
7950 $license->secret_key
7951 );
7952 } else {
7953 // Activate the license.
7954 $install = $this->get_api_site_scope()->call(
7955 '/',
7956 'put',
7957 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7958 );
7959
7960 if ( ! FS_Api::is_api_error( $install ) ) {
7961 $this->_sync_addon_license( $this->get_id(), true );
7962 }
7963 }
7964 }
7965
7966 /**
7967 * @author Leo Fajardo (@leorw)
7968 * @since 2.3.0
7969 *
7970 * @param FS_Plugin_License $license
7971 */
7972 private function maybe_network_activate_addon_license( $license = null ) {
7973 $parent_fs = $this->get_parent_instance();
7974 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7975 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7976 return;
7977 }
7978
7979 $license = ( ! is_null( $license ) ) ?
7980 $license :
7981 $this->get_active_parent_license();
7982
7983 if ( ! is_object( $license ) ) {
7984 return;
7985 }
7986
7987 if (
7988 $this->is_bundle_license_auto_activation_enabled() &&
7989 ! empty( $license->products )
7990 ) {
7991 $this->activate_bundle_license( $license );
7992
7993 return;
7994 }
7995
7996 if ( ! $this->is_network_registered() ) {
7997 $sites = $this->get_sites_for_network_level_optin();
7998
7999 if ( count( $sites ) > $license->left() ) {
8000 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
8001 return;
8002 }
8003
8004 // Opt in with a license key.
8005 $this->opt_in(
8006 $parent_fs->get_user()->email,
8007 false,
8008 false,
8009 $license->secret_key,
8010 false,
8011 false,
8012 false,
8013 null,
8014 $sites
8015 );
8016 } else {
8017 $blog_2_install_map = array();
8018 $site_ids = array();
8019
8020 $all_sites = Freemius::get_sites();
8021
8022 foreach ( $all_sites as $site ) {
8023 $blog_id = Freemius::get_site_blog_id( $site );
8024 $install = $this->get_install_by_blog_id( $blog_id );
8025
8026 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
8027 // Skip license activation for installs that are already associated with a license.
8028 continue;
8029 }
8030
8031 if ( is_object( $install ) ) {
8032 $blog_2_install_map[ $blog_id ] = $install;
8033 } else {
8034 $site_ids[] = $blog_id;
8035 }
8036 }
8037
8038 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
8039 return;
8040 }
8041
8042 $user = $this->get_current_or_network_user();
8043
8044 if ( ! empty( $blog_2_install_map ) ) {
8045 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
8046
8047 if ( true !== $result ) {
8048 return;
8049 }
8050 }
8051
8052 if ( ! empty( $site_ids ) ) {
8053 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
8054 }
8055 }
8056 }
8057
8058 /**
8059 * 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.
8060 *
8061 * @author Leo Fajardo (@leorw)
8062 * @since 2.4.0
8063 *
8064 * @param FS_Plugin_License $license
8065 * @param array $sites
8066 * @param int $blog_id
8067 */
8068 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8069 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8070 $license = $this->_license;
8071 }
8072
8073 if ( ! is_object( $license ) ) {
8074 return;
8075 }
8076
8077 $parent_license = ( ! empty( $license->products ) ) ?
8078 $license :
8079 $this->get_active_parent_license( $license->secret_key );
8080
8081 if ( is_object( $parent_license ) ) {
8082 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8083 }
8084 }
8085
8086 /**
8087 * Try to activate a bundle license for all the bundle products installed on the site.
8088 * (1) If a child product install already has a license, the bundle license won't be activated.
8089 * (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.
8090 * (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.
8091 *
8092 * @author Leo Fajardo (@leorw)
8093 * @since 2.4.0
8094 *
8095 * @param FS_Plugin_License $license
8096 * @param array $sites
8097 * @param int $current_blog_id
8098 */
8099 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8100 $is_network_admin = fs_is_network_admin();
8101
8102 $installs_by_blog_map = array();
8103 $site_info_by_blog_map = array();
8104
8105 /**
8106 * Try to activate the license for all supported products.
8107 *
8108 * @author Leo Fajardo
8109 */
8110 foreach ( $license->products as $product_id ) {
8111 $fs = self::get_instance_by_id( $product_id );
8112
8113 if ( ! is_object( $fs ) ) {
8114 continue;
8115 }
8116
8117 if ( ! $fs->has_paid_plan() ) {
8118 continue;
8119 }
8120
8121 if (
8122 ! $fs->is_addon() &&
8123 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8124 ) {
8125 /**
8126 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8127 * there is a context bundle.
8128 */
8129 continue;
8130 }
8131
8132 if ( $current_blog_id > 0 ) {
8133 $fs->switch_to_blog( $current_blog_id );
8134 }
8135
8136 if ( $fs->has_active_valid_license() ) {
8137 continue;
8138 }
8139
8140 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8141 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8142 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8143 continue;
8144 }
8145 } else {
8146 if ( ! $fs->is_network_active() ) {
8147 // Do not try to activate the license in the network level if the product is not network active.
8148 continue;
8149 }
8150
8151 if ( $fs->is_network_delegated_connection() ) {
8152 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8153 continue;
8154 }
8155
8156 $has_install_with_license = false;
8157
8158 // 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.
8159 $filtered_sites = array();
8160
8161 if ( empty( $sites ) ) {
8162 $all_sites = self::get_sites();
8163
8164 foreach ( $all_sites as $site ) {
8165 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8166 }
8167 } else {
8168 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8169 foreach ( $sites as $site ) {
8170 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8171 continue;
8172 }
8173
8174 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8175 }
8176 }
8177
8178 foreach ( $sites as $site ) {
8179 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8180 continue;
8181 }
8182
8183 $blog_id = $site['blog_id'];
8184
8185 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8186 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8187 }
8188
8189 $installs = $installs_by_blog_map[ $blog_id ];
8190 $install = null;
8191
8192 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8193 $install = $installs[ $fs->get_slug() ];
8194
8195 if (
8196 is_object( $install ) &&
8197 (
8198 ! FS_Site::is_valid_id( $install->id ) ||
8199 ! FS_User::is_valid_id( $install->user_id ) ||
8200 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8201 )
8202 ) {
8203 $install = null;
8204 }
8205 }
8206
8207 if (
8208 is_object( $install ) &&
8209 FS_Plugin_License::is_valid_id( $install->license_id )
8210 ) {
8211 $has_install_with_license = true;
8212 break;
8213 }
8214
8215 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8216 // Site activation delegated, don't activate bundle license on the site in the network admin.
8217 continue;
8218 }
8219
8220 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8221 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8222 }
8223
8224 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8225 }
8226
8227 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8228 // 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.
8229 continue;
8230 }
8231
8232 $sites = $filtered_sites;
8233 }
8234
8235 $fs->activate_migrated_license(
8236 $license->secret_key,
8237 null,
8238 null,
8239 $sites,
8240 ( $current_blog_id > 0 ? $current_blog_id : null )
8241 );
8242 }
8243 }
8244
8245 /**
8246 * Returns a parent license that can be activated for the context product.
8247 *
8248 * @author Leo Fajardo (@leorw)
8249 * @since 2.3.0
8250 *
8251 * @param string|null $license_key
8252 * @param bool $flush
8253 *
8254 * @return FS_Plugin_License
8255 */
8256 function get_active_parent_license( $license_key = null, $flush = true ) {
8257 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8258
8259 $fs = $this;
8260
8261 if ( $this->is_addon() ) {
8262 $parent_instance = $this->get_parent_instance();
8263
8264 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8265 $fs = $parent_instance;
8266 }
8267 }
8268
8269 $foreign_licenses = $fs->get_foreign_licenses_info(
8270 self::get_all_licenses( $this->get_parent_id() )
8271 );
8272
8273 if ( ! empty ( $foreign_licenses ) ) {
8274 $foreign_licenses = array(
8275 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8276 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8277 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8278 );
8279
8280 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8281 }
8282
8283 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8284
8285 if (
8286 ! $this->is_api_result_object( $result, 'licenses' ) ||
8287 ! is_array( $result->licenses ) ||
8288 empty( $result->licenses )
8289 ) {
8290 return null;
8291 }
8292
8293 $parent_license = null;
8294
8295 if ( empty( $license_key ) ) {
8296 $parent_license = $result->licenses[0];
8297 } else {
8298 foreach ( $result->licenses as $license ) {
8299 if ( $license_key === $license->secret_key ) {
8300 $parent_license = $license;
8301 break;
8302 }
8303 }
8304 }
8305
8306 if ( ! is_null( $parent_license ) ) {
8307 $parent_license = new FS_Plugin_License( $parent_license );
8308 }
8309
8310 return $parent_license;
8311 }
8312
8313 /**
8314 * @author Leo Fajardo (@leorw)
8315 * @since 2.3.0
8316 *
8317 * @return array
8318 */
8319 function get_sites_for_network_level_optin() {
8320 $sites = array();
8321 $all_sites = self::get_sites();
8322
8323 foreach ( $all_sites as $site ) {
8324 $blog_id = self::get_site_blog_id( $site );
8325
8326 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8327 ! $this->is_installed_on_site( $blog_id )
8328 ) {
8329 $sites[] = $this->get_site_info( $site );
8330 }
8331 }
8332
8333 return $sites;
8334 }
8335
8336 /**
8337 * Delete account.
8338 *
8339 * @author Vova Feldman (@svovaf)
8340 * @since 1.0.3
8341 *
8342 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8343 */
8344 function delete_account_event( $check_user = true ) {
8345 $this->_logger->entrance( 'slug = ' . $this->_slug );
8346
8347 if ( $check_user && ! $this->is_user_admin() ) {
8348 return;
8349 }
8350
8351 $this->do_action( 'before_account_delete' );
8352
8353 // Clear all admin notices.
8354 $this->_admin_notices->clear_all_sticky( false );
8355
8356 $this->_delete_site( false );
8357
8358 $delete_network_common_data = true;
8359
8360 if ( $this->_is_network_active ) {
8361 $installs = $this->get_blog_install_map();
8362
8363 // Don't delete common network data unless no other installs left.
8364 $delete_network_common_data = empty( $installs );
8365 }
8366
8367 if ( $delete_network_common_data ) {
8368 $this->_delete_plans( false );
8369
8370 $this->_delete_licenses( false );
8371
8372 // Delete add-ons related to plugin's account.
8373 $this->_delete_account_addons( false );
8374 }
8375
8376 // @todo Delete plans and licenses of add-ons.
8377
8378 self::$_accounts->store();
8379
8380 /**
8381 * IMPORTANT:
8382 * Clear crons must be executed before clearing all storage.
8383 * Otherwise, the cron will not be cleared.
8384 */
8385 if ( $delete_network_common_data ) {
8386 $this->clear_sync_cron();
8387 }
8388
8389 $this->clear_install_sync_cron();
8390
8391 // Clear all storage data.
8392 $this->_storage->clear_all( true, array(
8393 'is_delegated_connection',
8394 'connectivity_test',
8395 'is_on',
8396 ), false );
8397
8398 // Send delete event.
8399 $this->get_api_site_scope()->call( '/', 'delete' );
8400
8401 $this->do_action( 'after_account_delete' );
8402 }
8403
8404 /**
8405 * Delete network level account.
8406 *
8407 * @author Vova Feldman (@svovaf)
8408 * @since 2.0.0
8409 *
8410 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8411 */
8412 function delete_network_account_event( $check_user = true ) {
8413 $this->_logger->entrance( 'slug = ' . $this->_slug );
8414
8415 if ( $check_user && ! $this->is_user_admin() ) {
8416 return;
8417 }
8418
8419 $this->do_action( 'before_network_account_delete' );
8420
8421 // Clear all admin notices.
8422 $this->_admin_notices->clear_all_sticky();
8423
8424 $this->_delete_plans( false, false );
8425
8426 $this->_delete_licenses( false );
8427
8428 // Delete add-ons related to plugin's account.
8429 $this->_delete_account_addons( false );
8430
8431 // @todo Delete plans and licenses of add-ons.
8432
8433 self::$_accounts->store( true );
8434
8435 /**
8436 * IMPORTANT:
8437 * Clear crons must be executed before clearing all storage.
8438 * Otherwise, the cron will not be cleared.
8439 */
8440 $this->clear_sync_cron( true );
8441 $this->clear_install_sync_cron( true );
8442
8443 $sites = self::get_sites();
8444
8445 $install_ids = array();
8446 foreach ( $sites as $site ) {
8447 $blog_id = self::get_site_blog_id( $site );
8448
8449 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8450 continue;
8451 }
8452
8453 $install_id = $this->_delete_site( true, $blog_id );
8454
8455 // Clear all storage data.
8456 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8457
8458 if ( FS_Site::is_valid_id( $install_id ) ) {
8459 $install_ids[] = $install_id;
8460 }
8461
8462 switch_to_blog( $blog_id );
8463
8464 $this->do_action( 'after_account_delete' );
8465
8466 restore_current_blog();
8467 }
8468
8469 $this->_storage->clear_all( true, array(
8470 'connectivity_test',
8471 'is_on',
8472 ), true );
8473
8474 // Send delete event.
8475 if ( ! empty( $install_ids ) ) {
8476 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8477 }
8478
8479 $this->do_action( 'after_network_account_delete' );
8480 }
8481
8482 /**
8483 * Plugin deactivation hook.
8484 *
8485 * @author Vova Feldman (@svovaf)
8486 * @since 1.0.1
8487 */
8488 function _deactivate_plugin_hook() {
8489 $this->_logger->entrance( 'slug = ' . $this->_slug );
8490
8491 if ( ! $this->is_user_admin() ) {
8492 return;
8493 }
8494
8495 $is_network_deactivation = fs_is_network_admin();
8496 $storage_keys_for_removal = array();
8497
8498 $this->_admin_notices->clear_all_sticky();
8499
8500 $storage_keys_for_removal[] = 'sticky_optin_added';
8501 if ( isset( $this->_storage->sticky_optin_added ) ) {
8502 unset( $this->_storage->sticky_optin_added );
8503 }
8504
8505 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8506 // Remember that plugin was already installed.
8507 $this->_storage->is_plugin_new_install = false;
8508 }
8509
8510 // Hook to plugin uninstall.
8511 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8512
8513 $this->clear_module_main_file_cache();
8514 $this->clear_sync_cron( $this->_is_network_active );
8515 $this->clear_install_sync_cron();
8516
8517 if ( $this->is_registered() ) {
8518 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8519 FS_Plugin_Updater::instance( $this )->delete_update_data();
8520 }
8521
8522 if ( $is_network_deactivation ) {
8523 // Send deactivation event.
8524 $this->sync_installs( array(
8525 'is_active' => false,
8526 ) );
8527 } else {
8528 // Send deactivation event.
8529 $this->sync_install( array(
8530 'is_active' => false,
8531 ) );
8532 }
8533 } else {
8534 if ( ! $this->has_api_connectivity() ) {
8535 // Reset connectivity test cache.
8536 unset( $this->_storage->connectivity_test );
8537
8538 $storage_keys_for_removal[] = 'connectivity_test';
8539 }
8540 }
8541
8542 if ( $is_network_deactivation ) {
8543 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8544 unset( $this->_storage->sticky_optin_added_ms );
8545 }
8546
8547 if ( ! empty( $storage_keys_for_removal ) ) {
8548 $sites = self::get_sites();
8549
8550 foreach ( $sites as $site ) {
8551 $blog_id = self::get_site_blog_id( $site );
8552
8553 foreach ( $storage_keys_for_removal as $key ) {
8554 $this->_storage->remove( $key, false, $blog_id );
8555 }
8556
8557 $this->_storage->save( $blog_id );
8558 }
8559 }
8560 }
8561
8562 // Clear API cache on deactivation.
8563 FS_Api::clear_cache();
8564
8565 $this->remove_sdk_reference();
8566 }
8567
8568 /**
8569 * @author Vova Feldman (@svovaf)
8570 * @since 1.1.6
8571 */
8572 private function remove_sdk_reference() {
8573 global $fs_active_plugins;
8574
8575 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8576 if ( $this->_plugin_basename == $data->plugin_path ) {
8577 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8578 break;
8579 }
8580 }
8581
8582 fs_fallback_to_newest_active_sdk();
8583 }
8584
8585 /**
8586 * @author Vova Feldman (@svovaf)
8587 * @since 1.1.3
8588 *
8589 * @param bool $is_anonymous
8590 * @param bool|int $network_or_blog_id Since 2.0.0
8591 */
8592 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8593 // Store information regarding skip to try and opt-in the user
8594 // again in the future.
8595 $skip_info = array(
8596 'is' => $is_anonymous,
8597 'timestamp' => WP_FS__SCRIPT_START_TIME,
8598 'version' => $this->get_plugin_version(),
8599 );
8600
8601 if ( true === $network_or_blog_id ) {
8602 $this->_storage->is_anonymous_ms = $skip_info;
8603 } else {
8604 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8605 }
8606
8607 $this->network_upgrade_mode_completed();
8608
8609 // Update anonymous mode cache.
8610 $this->_is_anonymous = $is_anonymous;
8611 }
8612
8613 /**
8614 * @author Vova Feldman (@svovaf)
8615 * @since 2.0.0
8616 *
8617 * @param int $blog_id Site ID.
8618 * @param int $user_id User ID.
8619 * @param string $domain Site domain.
8620 * @param string $path Site path.
8621 * @param int $network_id Network ID. Only relevant on multi-network installations.
8622 * @param array $meta Metadata. Used to set initial site options.
8623 *
8624 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8625 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8626 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8627 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8628 */
8629 function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8630 $this->_logger->entrance();
8631
8632 if ( $this->is_premium() &&
8633 $this->is_network_connected() &&
8634 is_object( $this->_license ) &&
8635 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8636 $this->is_license_network_active( $blog_id )
8637 ) {
8638 /**
8639 * 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.
8640 */
8641 $current_blog_id = get_current_blog_id();
8642 $license = clone $this->_license;
8643
8644 $this->switch_to_blog( $blog_id );
8645
8646 // Opt-in with network user.
8647 $this->install_with_user(
8648 $this->get_network_user(),
8649 $license->secret_key,
8650 false,
8651 false,
8652 false
8653 );
8654
8655 if ( is_object( $this->_site ) ) {
8656 if ( $this->_site->license_id == $license->id ) {
8657 /**
8658 * If the license was activated successfully, sync the license data from the remote server.
8659 */
8660 $this->_license = $license;
8661 $this->sync_site_license();
8662 }
8663 }
8664
8665 $this->switch_to_blog( $current_blog_id );
8666
8667 if ( is_object( $this->_site ) ) {
8668 // Already connected (with or without a license), so no need to continue.
8669 return;
8670 }
8671 }
8672
8673 if ( $this->is_network_anonymous() ) {
8674 /**
8675 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8676 */
8677 $this->skip_site_connection( $blog_id );
8678 } else if ( $this->is_network_delegated_connection() ) {
8679 /**
8680 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8681 */
8682 $this->delegate_site_connection( $blog_id );
8683 } else if ( $this->is_network_connected() ) {
8684 /**
8685 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8686 */
8687 $current_blog_id = get_current_blog_id();
8688
8689 $this->switch_to_blog( $blog_id );
8690
8691 // Opt-in with network user.
8692 $this->install_with_user(
8693 $this->get_network_user(),
8694 false,
8695 false,
8696 false,
8697 false
8698 );
8699
8700 $this->switch_to_blog( $current_blog_id );
8701 } else {
8702 /**
8703 * If the super-admin mixed different options (connect, skip, delegated):
8704 * a) If at least one site connection was delegated, then automatically delegate connection.
8705 * 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.
8706 */
8707 $has_delegated_site = false;
8708
8709 $sites = self::get_sites();
8710 foreach ( $sites as $site ) {
8711 $blog_id = self::get_site_blog_id( $site );
8712
8713 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8714 $has_delegated_site = true;
8715 break;
8716 }
8717 }
8718
8719 if ( $has_delegated_site ) {
8720 $this->delegate_site_connection( $blog_id );
8721 } else {
8722 $this->skip_site_connection( $blog_id );
8723 }
8724 }
8725 }
8726
8727 /**
8728 * @author Vova Feldman (@svovaf)
8729 * @since 1.1.3
8730 *
8731 * @param bool|int $network_or_blog_id Since 2.0.0.
8732 */
8733 private function reset_anonymous_mode( $network_or_blog_id = 0 ) {
8734 if ( true === $network_or_blog_id ) {
8735 unset( $this->_storage->is_anonymous_ms );
8736 } else {
8737 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8738 }
8739
8740 /**
8741 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8742 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8743 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8744 *
8745 * @author Leo Fajardo (@leorw)
8746 * @since 1.2.2
8747 */
8748 if ( ! $this->_is_network_active ||
8749 0 === $network_or_blog_id ||
8750 get_current_blog_id() == $network_or_blog_id ||
8751 ( true === $network_or_blog_id && fs_is_network_admin() )
8752 ) {
8753 $this->_is_anonymous = null;
8754 }
8755 }
8756
8757 /**
8758 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8759 * deleting the account in the network level, the URL of the page to redirect to is correct.
8760 *
8761 * @author Leo Fajardo (@leorw)
8762 *
8763 * @since 2.1.3
8764 */
8765 private function maybe_set_slug_and_network_menu_exists_flag() {
8766 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8767 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8768 $this->_menu->get_slug() :
8769 $this->_slug
8770 );
8771 }
8772 }
8773
8774 /**
8775 * Clears the anonymous mode and redirects to the opt-in screen.
8776 *
8777 * @author Vova Feldman (@svovaf)
8778 * @since 1.1.7
8779 */
8780 function connect_again() {
8781 if ( ! $this->is_anonymous() ) {
8782 return;
8783 }
8784
8785 $this->reset_anonymous_mode( fs_is_network_admin() );
8786
8787 $this->maybe_set_slug_and_network_menu_exists_flag();
8788
8789 fs_redirect( $this->get_activation_url() );
8790 }
8791
8792 /**
8793 * Skip account connect, and set anonymous mode.
8794 *
8795 * @author Vova Feldman (@svovaf)
8796 * @since 1.1.1
8797 *
8798 * @param array|null $sites Since 2.0.0. Specific sites.
8799 * @param bool $skip_all_network Since 2.0.0. If true, skip connection for all sites.
8800 */
8801 function skip_connection( $sites = null, $skip_all_network = false ) {
8802 $this->_logger->entrance();
8803
8804 $this->_admin_notices->remove_sticky( 'connect_account' );
8805
8806 if ( $skip_all_network ) {
8807 $this->set_anonymous_mode( true, true );
8808 }
8809
8810 if ( ! $skip_all_network && empty( $sites ) ) {
8811 $this->skip_site_connection();
8812 } else {
8813 $uids = array();
8814
8815 if ( $skip_all_network ) {
8816 $this->set_anonymous_mode( true, true );
8817
8818 $sites = self::get_sites();
8819 foreach ( $sites as $site ) {
8820 $blog_id = self::get_site_blog_id( $site );
8821 $this->skip_site_connection( $blog_id, false );
8822 $uids[] = $this->get_anonymous_id( $blog_id );
8823 }
8824 } else if ( ! empty( $sites ) ) {
8825 foreach ( $sites as $site ) {
8826 $uids[] = $site['uid'];
8827 $this->skip_site_connection( $site['blog_id'], false );
8828 }
8829 }
8830
8831 // Send anonymous skip event.
8832 // No user identified info nor any tracking will be sent after the user skips the opt-in.
8833 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8834 'uids' => $uids,
8835 ) );
8836 }
8837
8838 $this->network_upgrade_mode_completed();
8839 }
8840
8841 /**
8842 * Skip connection for specific site in the network.
8843 *
8844 * @author Vova Feldman (@svovaf)
8845 * @since 2.0.0
8846 *
8847 * @param int|null $blog_id
8848 * @param bool $send_skip
8849 */
8850 private function skip_site_connection( $blog_id = null, $send_skip = true ) {
8851 $this->_logger->entrance();
8852
8853 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8854
8855 $this->set_anonymous_mode( true, $blog_id );
8856
8857 if ( $send_skip ) {
8858 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8859 'uids' => array( $this->get_anonymous_id( $blog_id ) ),
8860 ) );
8861 }
8862 }
8863
8864 /**
8865 * Plugin version update hook.
8866 *
8867 * @author Vova Feldman (@svovaf)
8868 * @since 1.0.4
8869 */
8870 private function update_plugin_version_event() {
8871 $this->_logger->entrance();
8872
8873 if ( ! $this->is_registered() ) {
8874 return;
8875 }
8876
8877 $this->schedule_install_sync();
8878 // $this->sync_install( array(), true );
8879 }
8880
8881 /**
8882 * Generate an MD5 signature of a plugins collection.
8883 * This helper methods used to identify changes in a plugins collection.
8884 *
8885 * @author Vova Feldman (@svovaf)
8886 * @since 2.0.0
8887 *
8888 * @param array [string]array $plugins
8889 *
8890 * @return string
8891 */
8892 private function get_plugins_thumbprint( $plugins ) {
8893 ksort( $plugins );
8894
8895 $thumbprint = '';
8896 foreach ( $plugins as $basename => $data ) {
8897 $thumbprint .= $data['slug'] . ',' .
8898 $data['Version'] . ',' .
8899 ( $data['is_active'] ? '1' : '0' ) . ';';
8900 }
8901
8902 return md5( $thumbprint );
8903 }
8904
8905 /**
8906 * Return a list of modified plugins since the last sync.
8907 *
8908 * Note:
8909 * There's no point to store a plugins counter since even if the number of
8910 * plugins didn't change, we still need to check if the versions are all the
8911 * same and the activity state is similar.
8912 *
8913 * @author Vova Feldman (@svovaf)
8914 * @since 1.1.8
8915 *
8916 * @return array|false
8917 */
8918 private function get_plugins_data_for_api() {
8919 // Alias.
8920 $site_active_plugins_option_name = 'active_plugins';
8921 $network_plugins_option_name = 'all_plugins';
8922
8923 /**
8924 * Collection of all site level active plugins.
8925 */
8926 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8927
8928 if ( ! is_object( $site_active_plugins_cache ) ) {
8929 $site_active_plugins_cache = (object) array(
8930 'timestamp' => '',
8931 'md5' => '',
8932 'plugins' => array(),
8933 );
8934 }
8935
8936 $time = time();
8937
8938 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8939 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8940 ) {
8941 // Don't send plugin updates if last update was in the past 5 min.
8942 return false;
8943 }
8944
8945 // Write timestamp to lock the logic.
8946 $site_active_plugins_cache->timestamp = $time;
8947 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8948
8949 // Reload options from DB.
8950 self::$_accounts->load( true );
8951 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8952
8953 if ( $time != $site_active_plugins_cache->timestamp ) {
8954 // If timestamp is different, then another thread captured the lock.
8955 return false;
8956 }
8957
8958 /**
8959 * Collection of all plugins (network level).
8960 */
8961 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8962
8963 if ( ! is_object( $network_plugins_cache ) ) {
8964 $network_plugins_cache = (object) array(
8965 'timestamp' => '',
8966 'md5' => '',
8967 'plugins' => array(),
8968 );
8969 }
8970
8971 // Check if there's a change in plugins.
8972 $network_plugins = self::get_network_plugins();
8973 $site_active_plugins = self::get_site_active_plugins();
8974
8975 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8976 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8977
8978 // Check if plugins status changed (version or active/inactive).
8979 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8980 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8981
8982 if ( ! $network_plugins_changed &&
8983 ! $site_active_plugins_changed
8984 ) {
8985 // No changes.
8986 return array();
8987 }
8988
8989 $plugins_update_data = array();
8990
8991 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8992 if ( ! isset( $network_plugins[ $basename ] ) ) {
8993 // Plugin uninstalled.
8994 $uninstalled_plugin_data = $data;
8995 $uninstalled_plugin_data['is_active'] = false;
8996 $uninstalled_plugin_data['is_uninstalled'] = true;
8997 $plugins_update_data[] = $uninstalled_plugin_data;
8998
8999 unset( $network_plugins[ $basename ] );
9000
9001 unset( $network_plugins_cache->plugins[ $basename ] );
9002 unset( $site_active_plugins_cache->plugins[ $basename ] );
9003
9004 continue;
9005 }
9006
9007 $was_active = $data['is_active'] ||
9008 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9009 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
9010 $is_active = $network_plugins[ $basename ]['is_active'] ||
9011 ( isset( $site_active_plugins[ $basename ] ) &&
9012 $site_active_plugins[ $basename ]['is_active'] );
9013
9014 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9015 isset( $site_active_plugins[ $basename ] )
9016 ) {
9017 // Plugin was site level activated.
9018 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
9019 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
9020 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9021 ! isset( $site_active_plugins[ $basename ] )
9022 ) {
9023 // Plugin was site level deactivated.
9024 unset( $site_active_plugins_cache->plugins[ $basename ] );
9025 }
9026
9027 $prev_version = $data['version'];
9028 $current_version = $network_plugins[ $basename ]['Version'];
9029
9030 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9031 // Plugin activated or deactivated, or version changed.
9032
9033 if ( $was_active !== $is_active ) {
9034 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9035 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9036 }
9037 }
9038
9039 if ( $prev_version !== $current_version ) {
9040 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9041 }
9042
9043 $updated_plugin_data = $data;
9044 $updated_plugin_data['is_active'] = $is_active;
9045 $updated_plugin_data['version'] = $current_version;
9046 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9047 $plugins_update_data[] = $updated_plugin_data;
9048 }
9049 }
9050
9051 // Find new plugins that weren't yet seen before.
9052 foreach ( $network_plugins as $basename => $data ) {
9053 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9054 // New plugin.
9055 $new_plugin = array(
9056 'slug' => $data['slug'],
9057 'version' => $data['Version'],
9058 'title' => $data['Name'],
9059 'is_active' => $data['is_active'],
9060 'is_uninstalled' => false,
9061 );
9062
9063 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9064
9065 $is_site_level_active = (
9066 isset( $site_active_plugins[ $basename ] ) &&
9067 $site_active_plugins[ $basename ]['is_active']
9068 );
9069
9070 /**
9071 * If not network active, set the activity status based on the site-level plugin status.
9072 */
9073 if ( ! $new_plugin['is_active'] ) {
9074 $new_plugin['is_active'] = $is_site_level_active;
9075 }
9076
9077 $plugins_update_data[] = $new_plugin;
9078
9079 if ( isset( $site_active_plugins[ $basename ] ) ) {
9080 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9081 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9082 }
9083 }
9084 }
9085
9086 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9087 $site_active_plugins_cache->timestamp = $time;
9088 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9089
9090 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9091 $network_plugins_cache->timestamp = $time;
9092 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9093
9094 return $plugins_update_data;
9095 }
9096
9097 /**
9098 * Return a list of modified themes since the last sync.
9099 *
9100 * Note:
9101 * There's no point to store a themes counter since even if the number of
9102 * themes didn't change, we still need to check if the versions are all the
9103 * same and the activity state is similar.
9104 *
9105 * @author Vova Feldman (@svovaf)
9106 * @since 1.1.8
9107 *
9108 * @return array|false
9109 */
9110 private function get_themes_data_for_api() {
9111 // Alias.
9112 $option_name = 'all_themes';
9113
9114 $all_cached_themes = self::$_accounts->get_option( $option_name );
9115
9116 if ( ! is_object( $all_cached_themes ) ) {
9117 $all_cached_themes = (object) array(
9118 'timestamp' => '',
9119 'md5' => '',
9120 'themes' => array(),
9121 );
9122 }
9123
9124 $time = time();
9125
9126 if ( ! empty( $all_cached_themes->timestamp ) &&
9127 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9128 ) {
9129 // Don't send theme updates if last update was in the past 5 min.
9130 return false;
9131 }
9132
9133 // Write timestamp to lock the logic.
9134 $all_cached_themes->timestamp = $time;
9135 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9136
9137 // Reload options from DB.
9138 self::$_accounts->load( true );
9139 $all_cached_themes = self::$_accounts->get_option( $option_name );
9140
9141 if ( $time != $all_cached_themes->timestamp ) {
9142 // If timestamp is different, then another thread captured the lock.
9143 return false;
9144 }
9145
9146 // Get active theme.
9147 $active_theme = wp_get_theme();
9148 $active_theme_stylesheet = $active_theme->get_stylesheet();
9149
9150 // Check if there's a change in themes.
9151 $all_themes = wp_get_themes();
9152
9153 // Check if themes changed.
9154 ksort( $all_themes );
9155
9156 $themes_signature = '';
9157 foreach ( $all_themes as $slug => $data ) {
9158 $is_active = ( $slug === $active_theme_stylesheet );
9159 $themes_signature .= $slug . ',' .
9160 $data->version . ',' .
9161 ( $is_active ? '1' : '0' ) . ';';
9162 }
9163
9164 // Check if themes status changed (version or active/inactive).
9165 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9166
9167 $themes_update_data = array();
9168
9169 if ( $themes_changed ) {
9170 // Change in themes, report changes.
9171
9172 // Update existing themes info.
9173 foreach ( $all_cached_themes->themes as $slug => $data ) {
9174 $is_active = ( $slug === $active_theme_stylesheet );
9175
9176 if ( ! isset( $all_themes[ $slug ] ) ) {
9177 // Plugin uninstalled.
9178 $uninstalled_theme_data = $data;
9179 $uninstalled_theme_data['is_active'] = false;
9180 $uninstalled_theme_data['is_uninstalled'] = true;
9181 $themes_update_data[] = $uninstalled_theme_data;
9182
9183 unset( $all_themes[ $slug ] );
9184 unset( $all_cached_themes->themes[ $slug ] );
9185 } else if ( $data['is_active'] !== $is_active ||
9186 $data['version'] !== $all_themes[ $slug ]->version
9187 ) {
9188 // Plugin activated or deactivated, or version changed.
9189
9190 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9191 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9192
9193 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9194 }
9195 }
9196
9197 // Find new themes that weren't yet seen before.
9198 foreach ( $all_themes as $slug => $data ) {
9199 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9200 $is_active = ( $slug === $active_theme_stylesheet );
9201
9202 // New plugin.
9203 $new_plugin = array(
9204 'slug' => $slug,
9205 'version' => $data->version,
9206 'title' => $data->name,
9207 'is_active' => $is_active,
9208 'is_uninstalled' => false,
9209 );
9210
9211 $themes_update_data[] = $new_plugin;
9212 $all_cached_themes->themes[ $slug ] = $new_plugin;
9213 }
9214 }
9215
9216 $all_cached_themes->md5 = md5( $themes_signature );
9217 $all_cached_themes->timestamp = time();
9218 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9219 }
9220
9221 return $themes_update_data;
9222 }
9223
9224 /**
9225 * Get site data for API install request.
9226 *
9227 * @author Vova Feldman (@svovaf)
9228 * @since 1.1.2
9229 *
9230 * @param string[] $override
9231 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9232 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9233 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, charset, title, and URL).
9234 *
9235 * @return array
9236 */
9237 private function get_install_data_for_api(
9238 array $override,
9239 $include_plugins = true,
9240 $include_themes = true,
9241 $include_blog_data = true
9242 ) {
9243 if ( $this->is_extensions_tracking_allowed() ) {
9244 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9245 /**
9246 * @since 1.1.8 Also send plugin updates.
9247 */
9248 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9249 $plugins = $this->get_plugins_data_for_api();
9250 if ( ! empty( $plugins ) ) {
9251 $override['plugins'] = $plugins;
9252 }
9253 }
9254 }
9255
9256 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9257 /**
9258 * @since 1.1.8 Also send themes updates.
9259 */
9260 if ( $include_themes && ! isset( $override['themes'] ) ) {
9261 $themes = $this->get_themes_data_for_api();
9262 if ( ! empty( $themes ) ) {
9263 $override['themes'] = $themes;
9264 }
9265 }
9266 }
9267 }
9268
9269 $versions = $this->get_versions();
9270
9271 $blog_data = $include_blog_data ?
9272 array(
9273 'language' => get_bloginfo( 'language' ),
9274 'charset' => get_bloginfo( 'charset' ),
9275 'title' => get_bloginfo( 'name' ),
9276 'url' => get_site_url(),
9277 ) :
9278 array();
9279
9280 return array_merge( $versions, $blog_data, array(
9281 'version' => $this->get_plugin_version(),
9282 'is_premium' => $this->is_premium(),
9283 // Special params.
9284 'is_active' => true,
9285 'is_disconnected' => $this->is_tracking_prohibited(),
9286 'is_uninstalled' => false,
9287 ), $override );
9288 }
9289
9290 /**
9291 * Update installs details.
9292 *
9293 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9294 *
9295 * @author Vova Feldman (@svovaf)
9296 * @since 2.0.0
9297 *
9298 * @param string[] string $override
9299 * @param bool $only_diff
9300 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9301 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9302 *
9303 * @return array
9304 */
9305 private function get_installs_data_for_api(
9306 array $override,
9307 $only_diff = false,
9308 $include_plugins = true,
9309 $include_themes = true
9310 ) {
9311 /**
9312 * @since 1.1.8 Also send plugin updates.
9313 */
9314 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9315 // $plugins = $this->get_plugins_data_for_api();
9316 // if ( ! empty( $plugins ) ) {
9317 // $override['plugins'] = $plugins;
9318 // }
9319 // }
9320 /**
9321 * @since 1.1.8 Also send themes updates.
9322 */
9323 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9324 // $themes = $this->get_themes_data_for_api();
9325 // if ( ! empty( $themes ) ) {
9326 // $override['themes'] = $themes;
9327 // }
9328 // }
9329
9330 // Common properties.
9331 $versions = $this->get_versions();
9332 $common = array_merge( $versions, array(
9333 'version' => $this->get_plugin_version(),
9334 'is_premium' => $this->is_premium(),
9335 ), $override );
9336
9337
9338 $is_common_diff_for_any_site = false;
9339 $common_diff_union = array();
9340
9341 $installs_data = array();
9342
9343 $sites = self::get_sites();
9344
9345 foreach ( $sites as $site ) {
9346 $blog_id = self::get_site_blog_id( $site );
9347
9348 $install = $this->get_install_by_blog_id( $blog_id );
9349
9350 if ( is_object( $install ) ) {
9351 if ( $install->user_id != $this->_user->id ) {
9352 // Install belongs to a different owner.
9353 continue;
9354 }
9355
9356 if ( ! $this->is_premium() && $install->is_tracking_prohibited() ) {
9357 // Don't send updates regarding opted-out installs.
9358 continue;
9359 }
9360
9361 $install_data = $this->get_site_info( $site );
9362
9363 $uid = $install_data['uid'];
9364
9365 unset( $install_data['blog_id'] );
9366 unset( $install_data['uid'] );
9367
9368 $install_data['is_disconnected'] = $install->is_disconnected;
9369 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9370 $install_data['is_uninstalled'] = $install->is_uninstalled;
9371
9372 $common_diff = null;
9373 $is_common_diff = false;
9374 if ( $only_diff ) {
9375 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9376 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9377
9378 $is_common_diff = ! empty( $common_diff );
9379
9380 if ( $is_common_diff ) {
9381 foreach ( $common_diff as $k => $v ) {
9382 if ( ! isset( $common_diff_union[ $k ] ) ) {
9383 $common_diff_union[ $k ] = $v;
9384 }
9385 }
9386 }
9387
9388 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9389 }
9390
9391 if ( ! empty( $install_data ) || $is_common_diff ) {
9392 // Add install ID and site unique ID.
9393 $install_data['id'] = $install->id;
9394 $install_data['uid'] = $uid;
9395
9396 $installs_data[] = $install_data;
9397 }
9398 }
9399 }
9400
9401 restore_current_blog();
9402
9403 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9404 if ( ! $only_diff ) {
9405 $installs_data[] = $common;
9406 } else if ( ! empty( $common_diff_union ) ) {
9407 $installs_data[] = $common_diff_union;
9408 }
9409 }
9410
9411 foreach ( $installs_data as &$data ) {
9412 $data = (object) $data;
9413 }
9414
9415 return $installs_data;
9416 }
9417
9418 /**
9419 * Compare site actual data to the stored install data and return the differences for an API data sync.
9420 *
9421 * @author Vova Feldman (@svovaf)
9422 * @since 2.0.0
9423 *
9424 * @param array $site
9425 * @param FS_Site $install
9426 * @param string[] string $override
9427 *
9428 * @return array
9429 */
9430 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9431 $diff = array();
9432 $special = array();
9433 $special_override = false;
9434
9435 foreach ( $site as $p => $v ) {
9436 if ( property_exists( $install, $p ) ) {
9437 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9438 $install->{$p} != $v
9439 ) {
9440 $install->{$p} = $v;
9441 $diff[ $p ] = $v;
9442 }
9443 } else {
9444 $special[ $p ] = $v;
9445
9446 if ( isset( $override[ $p ] ) ||
9447 'plugins' === $p ||
9448 'themes' === $p
9449 ) {
9450 $special_override = true;
9451 }
9452 }
9453 }
9454
9455 if ( $special_override || 0 < count( $diff ) ) {
9456 // Add special params only if has at least one
9457 // standard param, or if explicitly requested to
9458 // override a special param or a param which is not exist
9459 // in the install object.
9460 $diff = array_merge( $diff, $special );
9461 }
9462
9463 return $diff;
9464 }
9465
9466 /**
9467 * Update install only if changed.
9468 *
9469 * @author Vova Feldman (@svovaf)
9470 * @since 1.0.9
9471 *
9472 * @param string[] string $override
9473 * @param bool $flush
9474 *
9475 * @return false|object|string
9476 */
9477 private function send_install_update( $override = array(), $flush = false ) {
9478 $this->_logger->entrance();
9479
9480 $check_properties = $this->get_install_data_for_api( $override );
9481
9482 if ( $flush ) {
9483 $params = $check_properties;
9484 } else {
9485 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9486 }
9487
9488 $keepalive_only_update = false;
9489 if ( empty( $params ) ) {
9490 $keepalive_only_update = $this->should_send_keepalive_update();
9491
9492 if ( ! $keepalive_only_update ) {
9493 /**
9494 * There are no updates to send including keepalive.
9495 *
9496 * @author Leo Fajardo (@leorw)
9497 * @since 2.2.3
9498 */
9499 return false;
9500 }
9501 }
9502
9503 if ( ! $keepalive_only_update ) {
9504 /**
9505 * Do not update the last install sync timestamp after a keepalive-only call since there were no actual
9506 * updates sent.
9507 *
9508 * @author Leo Fajardo (@leorw)
9509 * @since 2.2.3
9510 */
9511 if ( ! is_multisite() ) {
9512 // Update last install sync timestamp.
9513 $this->set_cron_execution_timestamp( 'install_sync' );
9514 }
9515
9516 $params['uid'] = $this->get_anonymous_id();
9517 }
9518
9519 $this->set_keepalive_timestamp();
9520
9521 // Send updated values to FS.
9522 $site = $this->get_api_site_scope()->call( '/', 'put', $params );
9523
9524 if ( ! $keepalive_only_update && $this->is_api_result_entity( $site ) ) {
9525 /**
9526 * Do not clear scheduled sync after a keepalive-only call since there were no actual updates sent.
9527 *
9528 * @author Leo Fajardo (@leorw)
9529 * @since 2.2.3
9530 */
9531 if ( ! is_multisite() ) {
9532 // I successfully sent install update, clear scheduled sync if exist.
9533 $this->clear_install_sync_cron();
9534 }
9535 }
9536
9537 return $site;
9538 }
9539
9540 /**
9541 * Update installs only if changed.
9542 *
9543 * @author Vova Feldman (@svovaf)
9544 * @since 2.0.0
9545 *
9546 * @param string[] string $override
9547 * @param bool $flush
9548 *
9549 * @return false|object|string
9550 */
9551 private function send_installs_update( $override = array(), $flush = false ) {
9552 $this->_logger->entrance();
9553
9554 $installs_data = $this->get_installs_data_for_api( $override, ! $flush );
9555
9556 $keepalive_only_update = false;
9557 if ( empty( $installs_data ) ) {
9558 /**
9559 * Pass `true` to use the network level storage since the update is for many installs.
9560 *
9561 * @author Leo Fajardo (@leorw)
9562 * @since 2.2.3
9563 */
9564 $keepalive_only_update = $this->should_send_keepalive_update( true );
9565
9566 if ( ! $keepalive_only_update ) {
9567 /**
9568 * There are no updates to send including keepalive.
9569 *
9570 * @author Leo Fajardo (@leorw)
9571 * @since 2.2.3
9572 */
9573 return false;
9574 }
9575 }
9576
9577 if ( ! $keepalive_only_update ) {
9578 // Update last install sync timestamp if there were actual updates sent (i.e., not a keepalive-only call).
9579 $this->set_cron_execution_timestamp( 'install_sync' );
9580 }
9581
9582 /**
9583 * Pass `true` to use the network level storage since the update is for many installs.
9584 *
9585 * @author Leo Fajardo (@leorw)
9586 * @since 2.2.3
9587 */
9588 $this->set_keepalive_timestamp( true );
9589
9590 // Send updated values to FS.
9591 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9592
9593 if ( ! $keepalive_only_update && $this->is_api_result_object( $result, 'installs' ) ) {
9594 // 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.
9595 $this->clear_install_sync_cron();
9596 }
9597
9598 return $result;
9599 }
9600
9601 /**
9602 * @author Leo Fajardo (@leorw)
9603 *
9604 * @param bool|null $use_network_level_storage
9605 *
9606 * @return bool
9607 */
9608 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9609 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9610
9611 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9612 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9613 return true;
9614 } else {
9615 // 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.
9616 return ( 7 == rand( 1, 7 ) );
9617 }
9618 }
9619
9620 /**
9621 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9622 *
9623 * @author Leo Fajardo (@leorw)
9624 * @since 2.3.2
9625 */
9626 private function maybe_sync_install_user() {
9627 if ( $this->_user->id == $this->_site->user_id ) {
9628 return;
9629 }
9630
9631 // Fetch user data and store if found.
9632 $this->sync_user_by_current_install();
9633 }
9634
9635 /**
9636 * Update install only if changed.
9637 *
9638 * @author Vova Feldman (@svovaf)
9639 * @since 1.0.9
9640 *
9641 * @param string[] string $override
9642 * @param bool $flush
9643 */
9644 private function sync_install( $override = array(), $flush = false ) {
9645 $this->_logger->entrance();
9646
9647 $site = $this->send_install_update( $override, $flush );
9648
9649 if ( false === $site ) {
9650 // No sync required.
9651 return;
9652 }
9653
9654 if ( ! $this->is_api_result_entity( $site ) ) {
9655 // Failed to sync, don't update locally.
9656 return;
9657 }
9658
9659 $this->_site = new FS_Site( $site );
9660
9661 $this->_store_site( true );
9662 }
9663
9664 /**
9665 * Update install only if changed.
9666 *
9667 * @author Vova Feldman (@svovaf)
9668 * @since 1.0.9
9669 *
9670 * @param string[] string $override
9671 * @param bool $flush
9672 */
9673 private function sync_installs( $override = array(), $flush = false ) {
9674 $this->_logger->entrance();
9675
9676 $result = $this->send_installs_update( $override, $flush );
9677
9678 if ( false === $result ) {
9679 // No sync required.
9680 return;
9681 }
9682
9683 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9684 // Failed to sync, don't update locally.
9685 return;
9686 }
9687
9688 $address_to_blog_map = $this->get_address_to_blog_map();
9689
9690 foreach ( $result->installs as $install ) {
9691 $this->_site = new FS_Site( $install );
9692
9693 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9694 $blog_id = $address_to_blog_map[ $address ];
9695
9696 $this->_store_site( true, $blog_id );
9697 }
9698 }
9699
9700 /**
9701 * Track install's custom event.
9702 *
9703 * IMPORTANT:
9704 * Custom event tracking is currently only supported for specific clients.
9705 * If you are not one of them, please don't use this method. If you will,
9706 * the API will simply ignore your request based on the plugin ID.
9707 *
9708 * Need custom tracking for your plugin or theme?
9709 * If you are interested in custom event tracking please contact yo@freemius.com
9710 * for further details.
9711 *
9712 * @author Vova Feldman (@svovaf)
9713 * @since 1.2.1
9714 *
9715 * @param string $name Event name.
9716 * @param array $properties Associative key/value array with primitive values only
9717 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9718 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9719 *
9720 * @return object|false Event data or FALSE on failure.
9721 *
9722 * @throws \Freemius_InvalidArgumentException
9723 */
9724 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9725 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9726
9727 if ( ! $this->is_registered() ) {
9728 return false;
9729 }
9730
9731 $event = array( 'type' => $name );
9732
9733 if ( is_numeric( $process_at ) && $process_at > time() ) {
9734 $event['process_at'] = $process_at;
9735 }
9736
9737 if ( $once ) {
9738 $event['once'] = true;
9739 }
9740
9741 if ( ! empty( $properties ) ) {
9742 // Verify associative array values are primitive.
9743 foreach ( $properties as $k => $v ) {
9744 if ( ! is_scalar( $v ) ) {
9745 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9746 }
9747 }
9748
9749 $event['properties'] = $properties;
9750 }
9751
9752 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9753
9754 return $this->is_api_error( $result ) ?
9755 false :
9756 $result;
9757 }
9758
9759 /**
9760 * Track install's custom event only once, but it still triggers the API call.
9761 *
9762 * IMPORTANT:
9763 * Custom event tracking is currently only supported for specific clients.
9764 * If you are not one of them, please don't use this method. If you will,
9765 * the API will simply ignore your request based on the plugin ID.
9766 *
9767 * Need custom tracking for your plugin or theme?
9768 * If you are interested in custom event tracking please contact yo@freemius.com
9769 * for further details.
9770 *
9771 * @author Vova Feldman (@svovaf)
9772 * @since 1.2.1
9773 *
9774 * @param string $name Event name.
9775 * @param array $properties Associative key/value array with primitive values only
9776 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9777 *
9778 * @return object|false Event data or FALSE on failure.
9779 *
9780 * @throws \Freemius_InvalidArgumentException
9781 *
9782 * @user Freemius::track_event()
9783 */
9784 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9785 return $this->track_event( $name, $properties, $process_at, true );
9786 }
9787
9788 /**
9789 * Plugin uninstall hook.
9790 *
9791 * @author Vova Feldman (@svovaf)
9792 * @since 1.0.1
9793 *
9794 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9795 */
9796 function _uninstall_plugin_event( $check_user = true ) {
9797 $this->_logger->entrance( 'slug = ' . $this->_slug );
9798
9799 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9800 return;
9801 }
9802
9803 $params = array();
9804 $uninstall_reason = null;
9805 if ( isset( $this->_storage->uninstall_reason ) ) {
9806 $uninstall_reason = $this->_storage->uninstall_reason;
9807 $params['reason_id'] = $uninstall_reason->id;
9808 $params['reason_info'] = $uninstall_reason->info;
9809 }
9810
9811 if ( ! $this->is_registered() ) {
9812 // Send anonymous uninstall event only if user submitted a feedback.
9813 if ( isset( $uninstall_reason ) ) {
9814 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9815 $this->opt_in( false, false, false, false, true );
9816 } else {
9817 $params['uid'] = $this->get_anonymous_id();
9818 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9819 }
9820 }
9821 } else {
9822 $params = array_merge( $params, array(
9823 'is_active' => false,
9824 'is_uninstalled' => true,
9825 ) );
9826
9827 if ( $this->_is_network_active ) {
9828 // Send uninstall event.
9829 $this->send_installs_update( $params );
9830 } else {
9831 // Send uninstall event.
9832 $this->send_install_update( $params );
9833 }
9834 }
9835
9836 // @todo Decide if we want to delete plugin information from db.
9837 }
9838
9839 /**
9840 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9841 *
9842 * @author Vova Feldman (@svovaf)
9843 * @since 2.2.1
9844 *
9845 * @param string $is_premium
9846 * @param string $caller
9847 *
9848 * @return string
9849 */
9850 function set_basename( $is_premium, $caller ) {
9851 $basename = plugin_basename( $caller );
9852
9853 $current_basename = $is_premium ?
9854 $this->_premium_plugin_basename :
9855 $this->_free_plugin_basename;
9856
9857 if ( $current_basename == $basename ) {
9858 // Basename value set correctly.
9859 return;
9860 }
9861
9862 if ( $is_premium ) {
9863 $this->_premium_plugin_basename = $basename;
9864 } else {
9865 $this->_free_plugin_basename = $basename;
9866 }
9867
9868 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9869
9870 register_activation_hook(
9871 $plugin_dir . $basename,
9872 array( &$this, '_activate_plugin_event_hook' )
9873 );
9874 }
9875
9876 /**
9877 * @author Vova Feldman (@svovaf)
9878 * @since 1.1.1
9879 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9880 *
9881 * @return string
9882 */
9883 function premium_plugin_basename() {
9884 if ( ! isset( $this->_premium_plugin_basename ) ) {
9885 $this->_premium_plugin_basename = $this->is_premium() ?
9886 // The product is premium, so use the current basename.
9887 $this->_plugin_basename :
9888 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9889 }
9890
9891 return $this->_premium_plugin_basename;
9892 }
9893
9894 /**
9895 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9896 *
9897 * @author Vova Feldman (@svovaf)
9898 * @since 1.0.2
9899 */
9900 public static function _uninstall_plugin_hook() {
9901 self::_load_required_static();
9902
9903 self::$_static_logger->entrance();
9904
9905 if ( ! current_user_can( 'activate_plugins' ) ) {
9906 return;
9907 }
9908
9909 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9910
9911 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9912
9913 define( 'WP_FS__UNINSTALL_MODE', true );
9914
9915 $fs = self::get_instance_by_file( $plugin_file );
9916
9917 if ( is_object( $fs ) ) {
9918 $fs->remove_sdk_reference();
9919
9920 self::require_plugin_essentials();
9921
9922 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9923 is_plugin_active( $fs->premium_plugin_basename() )
9924 ) {
9925 // Deleting Free or Premium plugin version while the other version still installed.
9926 return;
9927 }
9928
9929 $fs->_uninstall_plugin_event();
9930
9931 $fs->do_action( 'after_uninstall' );
9932 }
9933 }
9934
9935 #----------------------------------------------------------------------------------
9936 #region Plugin Information
9937 #----------------------------------------------------------------------------------
9938
9939 /**
9940 * Load WordPress core plugin.php essential module.
9941 *
9942 * @author Vova Feldman (@svovaf)
9943 * @since 1.1.1
9944 */
9945 private static function require_plugin_essentials() {
9946 if ( ! function_exists( 'get_plugins' ) ) {
9947 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9948
9949 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9950 }
9951 }
9952
9953 /**
9954 * Load WordPress core pluggable.php module.
9955 *
9956 * @author Vova Feldman (@svovaf)
9957 * @since 1.1.2
9958 */
9959 private static function require_pluggable_essentials() {
9960 if ( ! function_exists( 'wp_get_current_user' ) ) {
9961 require_once ABSPATH . 'wp-includes/pluggable.php';
9962 }
9963 }
9964
9965 /**
9966 * Return plugin data.
9967 *
9968 * @author Vova Feldman (@svovaf)
9969 * @since 1.0.1
9970 *
9971 * @param bool $reparse_plugin_metadata
9972 *
9973 * @return array
9974 */
9975 function get_plugin_data( $reparse_plugin_metadata = false ) {
9976 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9977 self::require_plugin_essentials();
9978
9979 if ( $this->is_plugin() ) {
9980 /**
9981 * @author Vova Feldman (@svovaf)
9982 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9983 *
9984 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9985 */
9986 $plugin_data = get_plugin_data(
9987 $this->_plugin_main_file_path,
9988 false,
9989 false
9990 );
9991 } else {
9992 $theme_data = wp_get_theme();
9993
9994 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9995 $parent_theme = $theme_data->parent();
9996
9997 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9998 $theme_data = $parent_theme;
9999 }
10000 }
10001
10002 $plugin_data = array(
10003 'Name' => $theme_data->get( 'Name' ),
10004 'Version' => $theme_data->get( 'Version' ),
10005 'Author' => $theme_data->get( 'Author' ),
10006 'Description' => $theme_data->get( 'Description' ),
10007 'PluginURI' => $theme_data->get( 'ThemeURI' ),
10008 );
10009 }
10010
10011 $this->_plugin_data = $plugin_data;
10012 }
10013
10014 return $this->_plugin_data;
10015 }
10016
10017 /**
10018 * @author Vova Feldman (@svovaf)
10019 * @since 1.0.1
10020 * @since 1.2.2.5 If slug not set load slug by module ID.
10021 *
10022 * @return string Plugin slug.
10023 */
10024 function get_slug() {
10025 if ( ! isset( $this->_slug ) ) {
10026 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10027 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10028 }
10029
10030 return $this->_slug;
10031 }
10032
10033 /**
10034 * @author Leo Fajardo (@leorw)
10035 * @since 2.2.1
10036 *
10037 * @return string
10038 */
10039 function get_premium_slug() {
10040 return is_object( $this->_plugin ) ?
10041 $this->_plugin->premium_slug :
10042 "{$this->_slug}-premium";
10043 }
10044
10045 /**
10046 * Retrieve the desired folder name for the product.
10047 *
10048 * @author Vova Feldman (@svovaf)
10049 * @since 1.2.1.7
10050 *
10051 * @return string Plugin slug.
10052 */
10053 function get_target_folder_name() {
10054 return $this->can_use_premium_code() ?
10055 $this->_plugin->premium_slug :
10056 $this->_slug;
10057 }
10058
10059 /**
10060 * @author Vova Feldman (@svovaf)
10061 * @since 1.0.1
10062 *
10063 * @return number Plugin ID.
10064 */
10065 function get_id() {
10066 return $this->_plugin->id;
10067 }
10068
10069 /**
10070 * @author Leo Fajardo (@leorw)
10071 * @since 2.2.4
10072 *
10073 * @return number|null Bundle ID.
10074 */
10075 function get_bundle_id() {
10076 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10077 $this->_plugin->bundle_id :
10078 null;
10079 }
10080
10081 /**
10082 * @author Vova Feldman (@svovaf)
10083 * @since 2.3.1
10084 *
10085 * @return string|null Bundle public key.
10086 */
10087 function get_bundle_public_key() {
10088 return isset( $this->_plugin->bundle_public_key ) ?
10089 $this->_plugin->bundle_public_key :
10090 null;
10091 }
10092
10093 /**
10094 * @author Vova Feldman (@svovaf)
10095 * @since 1.2.1.5
10096 *
10097 * @return string Freemius SDK version
10098 */
10099 function get_sdk_version() {
10100 return $this->version;
10101 }
10102
10103 /**
10104 * @author Vova Feldman (@svovaf)
10105 * @since 1.2.1.5
10106 *
10107 * @return number Parent plugin ID (if parent exist).
10108 */
10109 function get_parent_id() {
10110 return $this->is_addon() ?
10111 $this->get_parent_instance()->get_id() :
10112 $this->_plugin->id;
10113 }
10114
10115 /**
10116 * @author Vova Feldman (@svovaf)
10117 * @since 2.3.1
10118 *
10119 * @return string
10120 */
10121 function get_usage_tracking_terms_url() {
10122 return $this->apply_filters(
10123 'usage_tracking_terms_url',
10124 "https://freemius.com/wordpress/usage-tracking/{$this->_plugin->id}/{$this->_slug}/"
10125 );
10126 }
10127
10128 /**
10129 * @author Vova Feldman (@svovaf)
10130 * @since 2.3.1
10131 *
10132 * @return string
10133 */
10134 function get_eula_url() {
10135 return $this->apply_filters(
10136 'eula_url',
10137 "https://freemius.com/terms/{$this->_plugin->id}/{$this->_slug}/"
10138 );
10139 }
10140
10141 /**
10142 * @author Vova Feldman (@svovaf)
10143 * @since 1.0.1
10144 *
10145 * @return string Plugin public key.
10146 */
10147 function get_public_key() {
10148 return $this->_plugin->public_key;
10149 }
10150
10151 /**
10152 * Will be available only on sandbox mode.
10153 *
10154 * @author Vova Feldman (@svovaf)
10155 * @since 1.0.4
10156 *
10157 * @return mixed Plugin secret key.
10158 */
10159 function get_secret_key() {
10160 return $this->_plugin->secret_key;
10161 }
10162
10163 /**
10164 * @author Vova Feldman (@svovaf)
10165 * @since 1.1.1
10166 *
10167 * @return bool
10168 */
10169 function has_secret_key() {
10170 return ! empty( $this->_plugin->secret_key );
10171 }
10172
10173 /**
10174 * @author Vova Feldman (@svovaf)
10175 * @since 1.0.9
10176 *
10177 * @param string|bool $premium_suffix
10178 *
10179 * @return string
10180 */
10181 function get_plugin_name( $premium_suffix = false ) {
10182 $this->_logger->entrance();
10183
10184 /**
10185 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10186 *
10187 * @author Vova Feldman
10188 */
10189 if ( ! isset( $this->_plugin_name ) ) {
10190 // Name is not yet set.
10191 $this->set_name( $premium_suffix );
10192 } else if (
10193 ! empty( $premium_suffix ) &&
10194 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10195 ) {
10196 // Name is already set, but there's a change in the premium suffix.
10197 $this->set_name( $premium_suffix );
10198 }
10199
10200 return $this->_plugin_name;
10201 }
10202
10203 /**
10204 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10205 *
10206 * @author Vova Feldman (@svovaf)
10207 * @since 2.2.1
10208 *
10209 * @param string $premium_suffix
10210 */
10211 private function set_name( $premium_suffix = '' ) {
10212 $plugin_data = $this->get_plugin_data();
10213
10214 // Get name.
10215 $this->_plugin_name = $plugin_data['Name'];
10216
10217 if ( is_string( $premium_suffix ) ) {
10218 $premium_suffix = trim( $premium_suffix );
10219
10220 if ( ! empty( $premium_suffix ) ) {
10221 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10222 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10223 $suffix_len = strlen( $suffix );
10224
10225 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10226 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10227 ) {
10228 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10229 }
10230 }
10231 }
10232
10233 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10234 }
10235
10236 /**
10237 * @author Vova Feldman (@svovaf)
10238 * @since 1.0.0
10239 *
10240 * @param bool $reparse_plugin_metadata
10241 *
10242 * @return string
10243 */
10244 function get_plugin_version( $reparse_plugin_metadata = false ) {
10245 $this->_logger->entrance();
10246
10247 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10248
10249 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10250
10251 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10252 }
10253
10254 /**
10255 * @author Vova Feldman (@svovaf)
10256 * @since 1.2.1.7
10257 *
10258 * @return string
10259 */
10260 function get_plugin_title() {
10261 $this->_logger->entrance();
10262
10263 $title = $this->_plugin->title;
10264
10265 return $this->apply_filters( 'plugin_title', $title );
10266 }
10267
10268 /**
10269 * @author Vova Feldman (@svovaf)
10270 * @since 1.2.2.7
10271 *
10272 * @param bool $lowercase
10273 *
10274 * @return string
10275 */
10276 function get_module_label( $lowercase = false ) {
10277 $label = $this->is_addon() ?
10278 $this->get_text_inline( 'Add-On', 'addon' ) :
10279 ( $this->is_plugin() ?
10280 $this->get_text_inline( 'Plugin', 'plugin' ) :
10281 $this->get_text_inline( 'Theme', 'theme' ) );
10282
10283 if ( $lowercase ) {
10284 $label = strtolower( $label );
10285 }
10286
10287 return $label;
10288 }
10289
10290 /**
10291 * @author Vova Feldman (@svovaf)
10292 * @since 1.0.4
10293 *
10294 * @return string
10295 */
10296 function get_plugin_basename() {
10297 if ( ! isset( $this->_plugin_basename ) ) {
10298 if ( $this->is_plugin() ) {
10299 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10300 } else {
10301 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10302 }
10303 }
10304
10305 return $this->_plugin_basename;
10306 }
10307
10308 function get_plugin_folder_name() {
10309 $this->_logger->entrance();
10310
10311 $plugin_folder = $this->_plugin_basename;
10312
10313 while ( '.' !== dirname( $plugin_folder ) ) {
10314 $plugin_folder = dirname( $plugin_folder );
10315 }
10316
10317 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10318
10319 return $plugin_folder;
10320 }
10321
10322 #endregion ------------------------------------------------------------------
10323
10324 /* Account
10325 ------------------------------------------------------------------------------------------------------------------*/
10326
10327 /**
10328 * Find plugin's slug by plugin's basename.
10329 *
10330 * @author Vova Feldman (@svovaf)
10331 * @since 1.0.9
10332 *
10333 * @param string $plugin_base_name
10334 *
10335 * @return false|string
10336 */
10337 private static function find_slug_by_basename( $plugin_base_name ) {
10338 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10339
10340 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10341 return false;
10342 }
10343
10344 return $file_slug_map[ $plugin_base_name ];
10345 }
10346
10347 /**
10348 * Store the map between the plugin's basename to the slug.
10349 *
10350 * @author Vova Feldman (@svovaf)
10351 * @since 1.0.9
10352 */
10353 private function store_file_slug_map() {
10354 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10355
10356 if ( ! array( $file_slug_map ) ) {
10357 $file_slug_map = array();
10358 }
10359
10360 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10361 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10362 ) {
10363 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10364 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10365 }
10366 }
10367
10368 /**
10369 * @return array[number]FS_User
10370 */
10371 static function get_all_users() {
10372 $users = self::maybe_get_entities_account_option( 'users', array() );
10373
10374 if ( ! is_array( $users ) ) {
10375 $users = array();
10376 }
10377
10378 return $users;
10379 }
10380
10381 /**
10382 * @param string $module_type
10383 * @param null|int $blog_id Since 2.0.0
10384 *
10385 * @return array[string]FS_Site
10386 */
10387 private static function get_all_sites(
10388 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10389 $blog_id = null
10390 ) {
10391 $sites = self::get_account_option( 'sites', $module_type, $blog_id );
10392
10393 if ( ! is_array( $sites ) ) {
10394 $sites = array();
10395 }
10396
10397 return $sites;
10398 }
10399
10400 /**
10401 * @author Leo Fajardo (@leorw)
10402 *
10403 * @since 1.2.2
10404 *
10405 * @param string $option_name
10406 * @param string $module_type
10407 * @param null|int $network_level_or_blog_id Since 2.0.0
10408 *
10409 * @return mixed
10410 */
10411 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10412 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10413 $option_name = $module_type . '_' . $option_name;
10414 }
10415
10416 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10417 }
10418
10419 /**
10420 * @author Leo Fajardo (@leorw)
10421 *
10422 * @since 1.2.2
10423 *
10424 * @param string $option_name
10425 * @param mixed $option_value
10426 * @param bool $store
10427 * @param null|int $network_level_or_blog_id Since 2.0.0
10428 */
10429 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10430 self::set_account_option_by_module(
10431 $this->_module_type,
10432 $option_name,
10433 $option_value,
10434 $store,
10435 $network_level_or_blog_id
10436 );
10437 }
10438
10439 /**
10440 * @author Vova Feldman (@svovaf)
10441 *
10442 * @since 1.2.2.7
10443 *
10444 * @param string $module_type
10445 * @param string $option_name
10446 * @param mixed $option_value
10447 * @param bool $store
10448 * @param null|int $network_level_or_blog_id Since 2.0.0
10449 */
10450 private static function set_account_option_by_module(
10451 $module_type,
10452 $option_name,
10453 $option_value,
10454 $store,
10455 $network_level_or_blog_id = null
10456 ) {
10457 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10458 $option_name = $module_type . '_' . $option_name;
10459 }
10460
10461 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10462 }
10463
10464 /**
10465 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10466 *
10467 * @author Leo Fajardo (@leorw)
10468 * @since 2.3.1
10469 *
10470 * @param string $option_name
10471 * @param mixed $default
10472 * @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).
10473 *
10474 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10475 */
10476 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10477 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10478
10479 $class_name = '';
10480
10481 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10482 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10483 }
10484
10485 switch ( $option_name ) {
10486 case 'plugins':
10487 case 'themes':
10488 case 'addons':
10489 $class_name = FS_Plugin::get_class_name();
10490 break;
10491 case 'users':
10492 $class_name = FS_User::get_class_name();
10493 break;
10494 case 'sites':
10495 $class_name = FS_Site::get_class_name();
10496 break;
10497 case 'licenses':
10498 case 'all_licenses':
10499 $class_name = FS_Plugin_License::get_class_name();
10500 break;
10501 case 'plans':
10502 $class_name = FS_Plugin_Plan::get_class_name();
10503 break;
10504 case 'updates':
10505 $class_name = FS_Plugin_Tag::get_class_name();
10506 break;
10507 }
10508
10509 if ( empty( $class_name ) ) {
10510 return $option;
10511 }
10512
10513 return fs_get_entities( $option, $class_name );
10514 }
10515
10516 /**
10517 * @author Vova Feldman (@svovaf)
10518 * @since 1.0.6
10519 *
10520 * @param number|null $module_id
10521 *
10522 * @return FS_Plugin_License[]
10523 */
10524 private static function get_all_licenses( $module_id = null ) {
10525 $licenses = self::get_account_option( 'all_licenses' );
10526
10527 if ( ! is_array( $licenses ) ) {
10528 $licenses = array();
10529 }
10530
10531 if ( is_null( $module_id ) ) {
10532 return $licenses;
10533 }
10534
10535 $licenses = isset( $licenses[ $module_id ] ) ?
10536 $licenses[ $module_id ] :
10537 array();
10538
10539 return $licenses;
10540 }
10541
10542 /**
10543 * @author Leo Fajardo (@leorw)
10544 * @since 2.0.0
10545 *
10546 * @return array
10547 */
10548 private static function get_all_licenses_by_module_type() {
10549 $licenses = self::get_account_option( 'all_licenses' );
10550
10551 $licenses_by_module_type = array(
10552 WP_FS__MODULE_TYPE_PLUGIN => array(),
10553 WP_FS__MODULE_TYPE_THEME => array()
10554 );
10555
10556 if ( ! is_array( $licenses ) ) {
10557 return $licenses_by_module_type;
10558 }
10559
10560 foreach ( $licenses as $module_id => $module_licenses ) {
10561 $fs = self::get_instance_by_id( $module_id );
10562 if ( false === $fs ) {
10563 continue;
10564 }
10565
10566 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10567 }
10568
10569 return $licenses_by_module_type;
10570 }
10571
10572 /**
10573 * @author Leo Fajardo (@leorw)
10574 * @since 2.0.0
10575 *
10576 * @param number $module_id
10577 * @param number|null $user_id
10578 *
10579 * @return array
10580 */
10581 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10582 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10583
10584 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10585 $all_modules_user_id_license_ids_map = array();
10586 }
10587
10588 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10589 $all_modules_user_id_license_ids_map[ $module_id ] :
10590 array();
10591
10592 if ( FS_User::is_valid_id( $user_id ) ) {
10593 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10594 $user_id_license_ids_map[ $user_id ] :
10595 array();
10596 }
10597
10598 return $user_id_license_ids_map;
10599 }
10600
10601 /**
10602 * @author Leo Fajardo (@leorw)
10603 * @since 2.0.0
10604 *
10605 * @param array $new_user_id_license_ids_map
10606 * @param number $module_id
10607 * @param number|null $user_id
10608 */
10609 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10610 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10611 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10612 $all_modules_user_id_license_ids_map = array();
10613 }
10614
10615 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10616 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10617 }
10618
10619 if ( FS_User::is_valid_id( $user_id ) ) {
10620 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10621 } else {
10622 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10623 }
10624
10625 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10626 }
10627
10628 /**
10629 * Get a collection of the user's linked license IDs.
10630 *
10631 * @author Vova Feldman (@svovaf)
10632 * @since 2.0.0
10633 *
10634 * @param number $user_id
10635 *
10636 * @return number[]
10637 */
10638 private function get_user_linked_license_ids( $user_id ) {
10639 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10640 }
10641
10642 /**
10643 * Override the user's linked license IDs with a new IDs collection.
10644 *
10645 * @author Vova Feldman (@svovaf)
10646 * @since 2.0.0
10647 *
10648 * @param number $user_id
10649 * @param number[] $license_ids
10650 */
10651 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10652 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10653 }
10654
10655 /**
10656 * Link a specified license ID to a given user.
10657 *
10658 * @author Vova Feldman (@svovaf)
10659 * @since 2.0.0
10660 *
10661 * @param number $license_id
10662 * @param number $user_id
10663 */
10664 private function link_license_2_user( $license_id, $user_id ) {
10665 $license_ids = $this->get_user_linked_license_ids( $user_id );
10666
10667 if ( in_array( $license_id, $license_ids ) ) {
10668 // License already linked.
10669 return;
10670 }
10671
10672 $license_ids[] = $license_id;
10673
10674 $this->set_user_linked_license_ids( $user_id, $license_ids );
10675 }
10676
10677 /**
10678 * @param string|bool $module_type
10679 *
10680 * @return FS_Plugin_Plan[]
10681 */
10682 private static function get_all_plans( $module_type = false ) {
10683 $plans = self::get_account_option( 'plans', $module_type );
10684
10685 if ( ! is_array( $plans ) ) {
10686 $plans = array();
10687 }
10688
10689 return $plans;
10690 }
10691
10692 /**
10693 * @author Vova Feldman (@svovaf)
10694 * @since 1.0.4
10695 *
10696 * @return FS_Plugin_Tag[]
10697 */
10698 private static function get_all_updates() {
10699 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10700
10701 if ( ! is_array( $updates ) ) {
10702 $updates = array();
10703 }
10704
10705 return $updates;
10706 }
10707
10708 /**
10709 * @author Vova Feldman (@svovaf)
10710 * @since 1.0.6
10711 *
10712 * @return array<number,FS_Plugin[]>|false
10713 */
10714 private static function get_all_addons() {
10715 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10716
10717 if ( ! is_array( $addons ) ) {
10718 $addons = array();
10719 }
10720
10721 return $addons;
10722 }
10723
10724 /**
10725 * @author Vova Feldman (@svovaf)
10726 * @since 1.0.6
10727 *
10728 * @return number[]|false
10729 */
10730 private static function get_all_account_addons() {
10731 $addons = self::$_accounts->get_option( 'account_addons', array() );
10732
10733 if ( ! is_array( $addons ) ) {
10734 $addons = array();
10735 }
10736
10737 return $addons;
10738 }
10739
10740 /**
10741 * Check if user has connected his account (opted-in).
10742 *
10743 * Note:
10744 * If the user opted-in and opted-out on a later stage,
10745 * this will still return true. If you want to check if the
10746 * user is currently opted-in, use:
10747 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10748 *
10749 * @author Vova Feldman (@svovaf)
10750 * @since 1.0.1
10751 * @return bool
10752 */
10753 function is_registered() {
10754 return is_object( $this->_user );
10755 }
10756
10757 /**
10758 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10759 *
10760 * @author Leo Fajardo (@leorw)
10761 * @since 1.2.1.5
10762 *
10763 * @return bool
10764 */
10765 function is_tracking_allowed() {
10766 return ( is_object( $this->_site ) && $this->_site->is_tracking_allowed() );
10767 }
10768
10769 /**
10770 * @author Leo Fajardo (@leorw)
10771 * @since 2.4.0
10772 *
10773 * @return bool
10774 */
10775 function is_bundle_license_auto_activation_enabled() {
10776 return $this->is_addon() ?
10777 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10778 $this->_is_bundle_license_auto_activation_enabled;
10779 }
10780
10781 /**
10782 * @author Vova Feldman (@svovaf)
10783 * @since 1.0.4
10784 *
10785 * @return FS_Plugin
10786 */
10787 function get_plugin() {
10788 return $this->_plugin;
10789 }
10790
10791 /**
10792 * @author Vova Feldman (@svovaf)
10793 * @since 1.0.3
10794 *
10795 * @return FS_User
10796 */
10797 function get_user() {
10798 return $this->_user;
10799 }
10800
10801 /**
10802 * @author Vova Feldman (@svovaf)
10803 * @since 1.0.3
10804 *
10805 * @return FS_Site
10806 */
10807 function get_site() {
10808 return $this->_site;
10809 }
10810
10811 /**
10812 * Get plugin add-ons.
10813 *
10814 * @author Vova Feldman (@svovaf)
10815 * @since 1.0.6
10816 *
10817 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10818 *
10819 * @param bool $flush
10820 *
10821 * @return FS_Plugin[]|false
10822 */
10823 function get_addons( $flush = false ) {
10824 $this->_logger->entrance();
10825
10826 if ( ! $this->_has_addons ) {
10827 return false;
10828 }
10829
10830 $addons = $this->sync_addons( $flush );
10831
10832 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10833 false :
10834 $addons;
10835 }
10836
10837 /**
10838 * @author Vova Feldman (@svovaf)
10839 * @since 1.0.6
10840 *
10841 * @return number[]|false
10842 */
10843 function get_account_addons() {
10844 $this->_logger->entrance();
10845
10846 $addons = self::get_all_account_addons();
10847
10848 if ( ! is_array( $addons ) ||
10849 ! isset( $addons[ $this->_plugin->id ] ) ||
10850 ! is_array( $addons[ $this->_plugin->id ] ) ||
10851 0 === count( $addons[ $this->_plugin->id ] )
10852 ) {
10853 return false;
10854 }
10855
10856 return $addons[ $this->_plugin->id ];
10857 }
10858
10859 /**
10860 * Check if user has any
10861 *
10862 * @author Vova Feldman (@svovaf)
10863 * @since 1.1.6
10864 *
10865 * @return bool
10866 */
10867 function has_account_addons() {
10868 $addons = $this->get_account_addons();
10869
10870 return is_array( $addons ) && ( 0 < count( $addons ) );
10871 }
10872
10873
10874 /**
10875 * Get add-on by ID (from local data).
10876 *
10877 * @author Vova Feldman (@svovaf)
10878 * @since 1.0.6
10879 *
10880 * @param number $id
10881 *
10882 * @return FS_Plugin|false
10883 */
10884 function get_addon( $id ) {
10885 $this->_logger->entrance();
10886
10887 $addons = $this->get_addons();
10888
10889 if ( is_array( $addons ) ) {
10890 foreach ( $addons as $addon ) {
10891 if ( $id == $addon->id ) {
10892 return $addon;
10893 }
10894 }
10895 }
10896
10897 return false;
10898 }
10899
10900 /**
10901 * Get add-on by slug (from local data).
10902 *
10903 * @author Vova Feldman (@svovaf)
10904 * @since 1.0.6
10905 *
10906 * @param string $slug
10907 *
10908 * @param bool $flush
10909 *
10910 * @return FS_Plugin|false
10911 */
10912 function get_addon_by_slug( $slug, $flush = false ) {
10913 $this->_logger->entrance();
10914
10915 $addons = $this->get_addons( $flush );
10916
10917 if ( is_array( $addons ) ) {
10918 foreach ( $addons as $addon ) {
10919 if ( $slug === $addon->slug ) {
10920 return $addon;
10921 }
10922 }
10923 }
10924
10925 return false;
10926 }
10927
10928 /**
10929 * @var array<number,object[]> {
10930 * @key number Add-on ID.
10931 * @val object[] The add-on's plans and prices object.
10932 * }
10933 */
10934 private $plans_and_pricing_by_addon_id;
10935
10936 /**
10937 * @author Leo Fajardo (@leorw)
10938 * @since 2.3.0
10939 *
10940 * @return array<number,object[]> {
10941 * @key number Add-on ID.
10942 * @val object[] The add-on's plans and prices object.
10943 * }
10944 */
10945 function _get_addons_plans_and_pricing_map_by_id() {
10946 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
10947 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
10948
10949 $plans_and_pricing_by_addon_id = array();
10950 if ( $this->is_api_result_object( $result, 'addons' ) ) {
10951 foreach ( $result->addons as $addon ) {
10952 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
10953 }
10954 }
10955
10956 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
10957 }
10958
10959 return $this->plans_and_pricing_by_addon_id;
10960 }
10961
10962 /**
10963 * @author Leo Fajardo (@leorw)
10964 * @since 2.3.0
10965 *
10966 * @param number $addon_id
10967 * @param bool $is_installed
10968 *
10969 * @return array
10970 */
10971 function _get_addon_info( $addon_id, $is_installed ) {
10972 $addon = $this->get_addon( $addon_id );
10973
10974 if ( ! is_object( $addon ) ) {
10975 // Unexpected call.
10976 return array();
10977 }
10978
10979 $slug = $addon->slug;
10980
10981 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
10982
10983 if ( ! fs_is_network_admin() ) {
10984 // Get blog-level activated installations.
10985 $sites = self::maybe_get_entities_account_option( 'sites', array() );
10986 } else {
10987 $sites = null;
10988
10989 if ( $this->is_addon_activated( $addon_id ) &&
10990 $this->get_addon_instance( $addon_id )->is_network_active()
10991 ) {
10992 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
10993 // Get network-level activated installations.
10994 $sites = self::maybe_get_entities_account_option(
10995 'sites',
10996 array(),
10997 $addon_storage->network_install_blog_id
10998 );
10999 }
11000 }
11001 }
11002
11003 $addon_info = array(
11004 'is_connected' => false,
11005 'slug' => $slug,
11006 'title' => $addon->title,
11007 'is_whitelabeled' => $addon_storage->is_whitelabeled
11008 );
11009
11010 if ( ! $is_installed ) {
11011 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11012
11013 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11014 $has_paid_plan = false;
11015 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11016
11017 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11018 foreach ( $plans as $plan ) {
11019 if ( isset( $plan->pricing ) &&
11020 is_array( $plan->pricing ) &&
11021 count( $plan->pricing ) > 0
11022 ) {
11023 $has_paid_plan = true;
11024 break;
11025 }
11026 }
11027 }
11028
11029 $addon_info['has_paid_plan'] = $has_paid_plan;
11030 }
11031 }
11032
11033 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11034 return $addon_info;
11035 }
11036
11037 $site = $sites[ $slug ];
11038
11039 $addon_info['is_connected'] = (
11040 ( $addon->parent_plugin_id == $this->get_id() ) &&
11041 is_object( $site ) &&
11042 FS_Site::is_valid_id( $site->id ) &&
11043 FS_User::is_valid_id( $site->user_id ) &&
11044 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11045 );
11046
11047 if ( $addon_info['is_connected'] && $is_installed ) {
11048 return $addon_info;
11049 }
11050
11051 $addon_info['site'] = $site;
11052
11053 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11054 if ( isset( $plugins_data[ $slug ] ) ) {
11055 $plugin_data = $plugins_data[ $slug ];
11056
11057 $addon_info['version'] = $plugin_data->version;
11058 }
11059
11060 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11061 if ( isset( $all_plans[ $slug ] ) ) {
11062 $plans = $all_plans[ $slug ];
11063
11064 foreach ( $plans as $plan ) {
11065 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11066 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11067 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11068 break;
11069 }
11070 }
11071 }
11072
11073 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11074 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11075 foreach ( $licenses[ $addon_id ] as $license ) {
11076 if ( $license->id == $site->license_id ) {
11077 $addon_info['license'] = $license;
11078 break;
11079 }
11080 }
11081 }
11082
11083 if ( isset( $addon_info['license'] ) ) {
11084 if ( isset( $addon_storage->subscriptions ) &&
11085 ! empty( $addon_storage->subscriptions )
11086 ) {
11087 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11088
11089 foreach ( $addon_subscriptions as $subscription ) {
11090 if ( $subscription->license_id == $site->license_id ) {
11091 $addon_info['subscription'] = $subscription;
11092 break;
11093 }
11094 }
11095 }
11096 }
11097
11098 return $addon_info;
11099 }
11100
11101 /**
11102 * @author Vova Feldman (@svovaf)
11103 * @since 2.0.0
11104 *
11105 * @param number $user_id
11106 *
11107 * @return FS_User
11108 */
11109 static function _get_user_by_id( $user_id ) {
11110 self::$_static_logger->entrance( "user_id = {$user_id}" );
11111
11112 $users = self::get_all_users();
11113
11114 if ( is_array( $users ) ) {
11115 if ( isset( $users[ $user_id ] ) &&
11116 $users[ $user_id ] instanceof FS_User &&
11117 $user_id == $users[ $user_id ]->id
11118 ) {
11119 return $users[ $user_id ];
11120 }
11121
11122 // If user wasn't found by the key, iterate over all the users collection.
11123 foreach ( $users as $user ) {
11124 /**
11125 * @var FS_User $user
11126 */
11127 if ( $user_id == $user->id ) {
11128 return $user;
11129 }
11130 }
11131 }
11132
11133 return null;
11134 }
11135
11136 /**
11137 * Checks if a Freemius user_id is associated with a super-admin.
11138 *
11139 * @author Vova Feldman (@svovaf)
11140 * @since 2.0.0
11141 *
11142 * @param number $user_id
11143 *
11144 * @return bool
11145 */
11146 private static function is_super_admin( $user_id ) {
11147 $is_super_admin = false;
11148
11149 $user = self::_get_user_by_id( $user_id );
11150
11151 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11152 self::require_pluggable_essentials();
11153
11154 $wp_user = get_user_by( 'email', $user->email );
11155
11156 if ( $wp_user instanceof WP_User ) {
11157 $super_admins = get_super_admins();
11158 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11159 }
11160 }
11161
11162 return $is_super_admin;
11163 }
11164
11165 #----------------------------------------------------------------------------------
11166 #region Plans & Licensing
11167 #----------------------------------------------------------------------------------
11168
11169 /**
11170 * Check if running premium plugin code.
11171 *
11172 * @author Vova Feldman (@svovaf)
11173 * @since 1.0.5
11174 *
11175 * @return bool
11176 */
11177 function is_premium() {
11178 /**
11179 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11180 * `register_constructor_hooks` method.
11181 *
11182 * @author Leo Fajardo (@leorw)
11183 * @since 2.2.3
11184 */
11185 return is_object( $this->_plugin ) ?
11186 $this->_plugin->is_premium :
11187 false;
11188 }
11189
11190 /**
11191 * Get site's plan ID.
11192 *
11193 * @author Vova Feldman (@svovaf)
11194 * @since 1.0.2
11195 *
11196 * @return number
11197 */
11198 function get_plan_id() {
11199 return $this->_site->plan_id;
11200 }
11201
11202 /**
11203 * Get site's plan title.
11204 *
11205 * @author Vova Feldman (@svovaf)
11206 * @since 1.0.2
11207 *
11208 * @return string
11209 */
11210 function get_plan_title() {
11211 $plan = $this->get_plan();
11212
11213 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11214 }
11215
11216 /**
11217 * Get site's plan name.
11218 *
11219 * @author Vova Feldman (@svovaf)
11220 * @since 2.0.0
11221 *
11222 * @return string
11223 */
11224 function get_plan_name() {
11225 $plan = $this->get_plan();
11226
11227 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11228 }
11229
11230 /**
11231 * @author Vova Feldman (@svovaf)
11232 * @since 1.0.9
11233 *
11234 * @return FS_Plugin_Plan|false
11235 */
11236 function get_plan() {
11237 if ( ! is_object( $this->_site ) ) {
11238 return false;
11239 }
11240
11241 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11242 $this->_get_plan_by_id( $this->_site->plan_id ) :
11243 false;
11244 }
11245
11246 /**
11247 * @author Vova Feldman (@svovaf)
11248 * @since 1.0.3
11249 *
11250 * @return bool
11251 */
11252 function is_trial() {
11253 $this->_logger->entrance();
11254
11255 if ( ! $this->is_registered() || ! is_object( $this->_site ) ) {
11256 return false;
11257 }
11258
11259 return $this->_site->is_trial();
11260 }
11261
11262 /**
11263 * Check if currently in a trial with payment method (credit card or paypal).
11264 *
11265 * @author Vova Feldman (@svovaf)
11266 * @since 1.1.7
11267 *
11268 * @return bool
11269 */
11270 function is_paid_trial() {
11271 $this->_logger->entrance();
11272
11273 if ( ! $this->is_trial() ) {
11274 return false;
11275 }
11276
11277 if ( ! $this->has_active_valid_license() ) {
11278 return false;
11279 }
11280
11281 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11282 return false;
11283 }
11284
11285 /**
11286 * @var FS_Subscription $subscription
11287 */
11288 $subscription = $this->_get_subscription( $this->_license->id );
11289
11290 return ( is_object( $subscription ) && $subscription->is_active() );
11291 }
11292
11293 /**
11294 * Check if trial already utilized.
11295 *
11296 * @since 1.0.9
11297 *
11298 * @return bool
11299 */
11300 function is_trial_utilized() {
11301 $this->_logger->entrance();
11302
11303 if ( ! $this->is_registered() ) {
11304 return false;
11305 }
11306
11307 return $this->_site->is_trial_utilized();
11308 }
11309
11310 /**
11311 * Get trial plan information (if in trial).
11312 *
11313 * @author Vova Feldman (@svovaf)
11314 * @since 1.0.9
11315 *
11316 * @return bool|FS_Plugin_Plan
11317 */
11318 function get_trial_plan() {
11319 $this->_logger->entrance();
11320
11321 if ( ! $this->is_trial() ) {
11322 return false;
11323 }
11324
11325 // Try to load plan from local cache.
11326 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11327
11328 if ( ! is_object( $trial_plan ) ) {
11329 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11330
11331 /**
11332 * If managed to fetch the plan, add it to the plans collection.
11333 */
11334 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11335 if ( ! is_array( $this->_plans ) ) {
11336 $this->_plans = array();
11337 }
11338
11339 $this->_plans[] = $trial_plan;
11340 $this->_store_plans();
11341 }
11342 }
11343
11344 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11345 return $trial_plan;
11346 }
11347
11348 /**
11349 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11350 */
11351 $trial_plan = new FS_Plugin_Plan();
11352 $trial_plan->id = $this->_site->trial_plan_id;
11353 $trial_plan->name = 'pro';
11354 $trial_plan->title = 'Pro';
11355
11356 return $trial_plan;
11357 }
11358
11359 /**
11360 * Check if the user has an activate, non-expired license on current plugin's install.
11361 *
11362 * @since 1.0.9
11363 *
11364 * @return bool
11365 */
11366 function is_paying() {
11367 $this->_logger->entrance();
11368
11369 if ( ! $this->is_registered() ) {
11370 return false;
11371 }
11372
11373 if ( ! $this->has_paid_plan() ) {
11374 return false;
11375 }
11376
11377 return (
11378 ! $this->is_trial() &&
11379 'free' !== $this->get_plan_name() &&
11380 $this->has_active_valid_license()
11381 );
11382 }
11383
11384 /**
11385 * @author Vova Feldman (@svovaf)
11386 * @since 1.0.4
11387 *
11388 * @return bool
11389 */
11390 function is_free_plan() {
11391 if ( ! $this->is_registered() ) {
11392 return true;
11393 }
11394
11395 if ( ! $this->has_paid_plan() ) {
11396 return true;
11397 }
11398
11399 return (
11400 'free' === $this->get_plan_name() ||
11401 ! $this->has_features_enabled_license()
11402 );
11403 }
11404
11405 /**
11406 * @author Vova Feldman (@svovaf)
11407 * @since 1.0.5
11408 *
11409 * @return bool
11410 */
11411 function _has_premium_license() {
11412 $this->_logger->entrance();
11413
11414 $premium_license = $this->_get_available_premium_license();
11415
11416 return ( false !== $premium_license );
11417 }
11418
11419 /**
11420 * Check if user has any licenses associated with the plugin (including expired or blocking).
11421 *
11422 * @author Vova Feldman (@svovaf)
11423 * @since 1.1.7.3
11424 *
11425 * @param bool $including_foreign
11426 *
11427 * @return bool
11428 */
11429 function has_any_license( $including_foreign = true ) {
11430 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11431 return false;
11432 }
11433
11434 if ( $including_foreign ) {
11435 return true;
11436 }
11437
11438 foreach ( $this->_licenses as $license ) {
11439 if ( $this->_user->id == $license->user_id ) {
11440 return true;
11441 }
11442 }
11443
11444 return false;
11445 }
11446
11447 /**
11448 * @author Vova Feldman (@svovaf)
11449 * @since 1.0.5
11450 *
11451 * @param bool|null $is_localhost
11452 *
11453 * @return FS_Plugin_License|false
11454 */
11455 function _get_available_premium_license( $is_localhost = null ) {
11456 $this->_logger->entrance();
11457
11458 $licenses = $this->get_available_premium_licenses( $is_localhost );
11459 if ( ! empty( $licenses ) ) {
11460 return $licenses[0];
11461 }
11462
11463 return false;
11464 }
11465
11466 /**
11467 * @author Vova Feldman (@svovaf)
11468 * @since 1.0.5
11469 *
11470 * @param bool|null $is_localhost
11471 *
11472 * @return FS_Plugin_License[]
11473 */
11474 function get_available_premium_licenses( $is_localhost = null ) {
11475 $this->_logger->entrance();
11476
11477 $licenses = array();
11478 if ( ! $this->has_paid_plan() ) {
11479 return $licenses;
11480 }
11481
11482 if ( is_array( $this->_licenses ) ) {
11483 foreach ( $this->_licenses as $license ) {
11484 if ( ! $license->can_activate( $is_localhost ) ) {
11485 continue;
11486 }
11487
11488 $licenses[] = $license;
11489 }
11490 }
11491
11492 return $licenses;
11493 }
11494
11495 /**
11496 * Sync local plugin plans with remote server.
11497 *
11498 * 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).
11499 *
11500 * @author Vova Feldman (@svovaf)
11501 * @since 1.0.5
11502 *
11503 * @return FS_Plugin_Plan[]|object
11504 */
11505 function _sync_plans() {
11506 $plans = $this->_fetch_plugin_plans();
11507
11508 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11509 $plans_map = array();
11510 foreach ( $plans as $plan ) {
11511 $plans_map[ $plan->id ] = true;
11512 }
11513
11514 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11515
11516 foreach ( $plans_ids_to_keep as $plan_id ) {
11517 if ( isset( $plans_map[ $plan_id ] ) ) {
11518 continue;
11519 }
11520
11521 $missing_plan = self::_get_plan_by_id( $plan_id );
11522
11523 if ( is_object( $missing_plan ) ) {
11524 $plans[] = $missing_plan;
11525 }
11526 }
11527
11528 $this->_plans = $plans;
11529 $this->_store_plans();
11530 }
11531
11532 $this->do_action( 'after_plans_sync', $plans );
11533
11534 return $this->_plans;
11535 }
11536
11537 /**
11538 * Check if specified plan exists locally. If not, fetch it and store it.
11539 *
11540 * @author Vova Feldman (@svovaf)
11541 * @since 2.0.0
11542 *
11543 * @param number $plan_id
11544 *
11545 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11546 */
11547 private function sync_plan_if_not_exist( $plan_id ) {
11548 $plan = self::_get_plan_by_id( $plan_id );
11549
11550 if ( is_object( $plan ) ) {
11551 // Plan already exists.
11552 return $plan;
11553 }
11554
11555 $plan = $this->fetch_plan_by_id( $plan_id );
11556
11557 if ( $plan instanceof FS_Plugin_Plan ) {
11558 $this->_plans[] = $plan;
11559 $this->_store_plans();
11560
11561 return $plan;
11562 }
11563
11564 return $plan;
11565 }
11566
11567 /**
11568 * Check if specified license exists locally. If not, fetch it and store it.
11569 *
11570 * @author Vova Feldman (@svovaf)
11571 * @since 2.0.0
11572 *
11573 * @param number $license_id
11574 * @param string $license_key
11575 *
11576 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11577 */
11578 private function sync_license_if_not_exist( $license_id, $license_key ) {
11579 $license = $this->_get_license_by_id( $license_id );
11580
11581 if ( is_object( $license ) ) {
11582 // License already exists.
11583 return $license;
11584 }
11585
11586 $license = $this->fetch_license_by_key( $license_id, $license_key );
11587
11588 if ( $license instanceof FS_Plugin_License ) {
11589 $this->_licenses[] = $license;
11590
11591 $this->set_license( $license );
11592
11593 $this->_store_licenses();
11594
11595 return $license;
11596 }
11597
11598 return $license;
11599 }
11600
11601 /**
11602 * Get a collection of unique plan IDs that are associated with any installs in the network.
11603 *
11604 * @author Leo Fajardo (@leorw)
11605 * @since 2.0.0
11606 *
11607 * @return number[]
11608 */
11609 private function get_plans_ids_associated_with_installs() {
11610 if ( ! is_multisite() ) {
11611 if ( ! is_object( $this->_site ) ||
11612 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11613 ) {
11614 return array();
11615 }
11616
11617 return array( $this->_site->plan_id );
11618 }
11619
11620 $plan_ids = array();
11621 $sites = self::get_sites();
11622 foreach ( $sites as $site ) {
11623 $blog_id = self::get_site_blog_id( $site );
11624 $install = $this->get_install_by_blog_id( $blog_id );
11625
11626 if ( ! is_object( $install ) ||
11627 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11628 ) {
11629 continue;
11630 }
11631
11632 $plan_ids[ $install->plan_id ] = true;
11633 }
11634
11635 return array_keys( $plan_ids );
11636 }
11637
11638 /**
11639 * Get a collection of unique license IDs that are associated with any installs in the network.
11640 *
11641 * @author Leo Fajardo (@leorw)
11642 * @since 2.0.0
11643 *
11644 * @return number[]
11645 */
11646 private function get_license_ids_associated_with_installs() {
11647 if ( ! $this->_is_network_active ) {
11648 if ( ! is_object( $this->_site ) ||
11649 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11650 ) {
11651 return array();
11652 }
11653
11654 return array( $this->_site->license_id );
11655 }
11656
11657 $license_ids = array();
11658 $sites = self::get_sites();
11659 foreach ( $sites as $site ) {
11660 $blog_id = self::get_site_blog_id( $site );
11661 $install = $this->get_install_by_blog_id( $blog_id );
11662
11663 if ( ! is_object( $install ) ||
11664 ! FS_Plugin_License::is_valid_id( $install->license_id )
11665 ) {
11666 continue;
11667 }
11668
11669 $license_ids[ $install->license_id ] = true;
11670 }
11671
11672 return array_keys( $license_ids );
11673 }
11674
11675 /**
11676 * @author Vova Feldman (@svovaf)
11677 * @since 1.0.5
11678 *
11679 * @param number $id
11680 *
11681 * @return FS_Plugin_Plan|false
11682 */
11683 function _get_plan_by_id( $id ) {
11684 $this->_logger->entrance();
11685
11686 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11687 $this->_sync_plans();
11688 }
11689
11690 foreach ( $this->_plans as $plan ) {
11691 if ( $id == $plan->id ) {
11692 return $plan;
11693 }
11694 }
11695
11696 return false;
11697 }
11698
11699 /**
11700 * @author Vova Feldman (@svovaf)
11701 * @since 1.1.8.1
11702 *
11703 * @param string $name
11704 *
11705 * @return FS_Plugin_Plan|false
11706 */
11707 private function get_plan_by_name( $name ) {
11708 $this->_logger->entrance();
11709
11710 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11711 $this->_sync_plans();
11712 }
11713
11714 foreach ( $this->_plans as $plan ) {
11715 if ( $name == $plan->name ) {
11716 return $plan;
11717 }
11718 }
11719
11720 return false;
11721 }
11722
11723 /**
11724 * Sync local licenses with remote server.
11725 *
11726 * @author Vova Feldman (@svovaf)
11727 * @since 1.0.6
11728 *
11729 * @param number|bool $site_license_id
11730 * @param number|null $blog_id
11731 *
11732 * @return FS_Plugin_License[]|object
11733 */
11734 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11735 $this->_logger->entrance();
11736
11737 $is_network_admin = fs_is_network_admin();
11738
11739 if ( $is_network_admin && is_null( $blog_id ) ) {
11740 $all_licenses = self::get_all_licenses( $this->_module_id );
11741 } else {
11742 $all_licenses = $this->get_user_licenses( $this->_user->id );
11743 }
11744
11745 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11746
11747 $all_licenses_map = array();
11748 foreach ( $all_licenses as $license ) {
11749 $all_licenses_map[ $license->id ] = true;
11750 }
11751
11752 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11753
11754 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11755 $licenses_map = array();
11756 foreach ( $licenses as $license ) {
11757 $licenses_map[ $license->id ] = true;
11758 }
11759
11760 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11761 // foreach ( $license_ids_to_keep as $license_id ) {
11762 // if ( isset( $licenses_map[ $license_id ] ) ) {
11763 // continue;
11764 // }
11765 //
11766 // $missing_license = self::_get_license_by_id( $license_id, false );
11767 // if ( is_object( $missing_license ) ) {
11768 // $licenses[] = $missing_license;
11769 // $licenses_map[ $missing_license->id ] = true;
11770 // }
11771 // }
11772
11773 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11774
11775 foreach ( $user_license_ids as $key => $license_id ) {
11776 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11777 // Remove access to licenses that no longer exist.
11778 unset( $user_license_ids[ $key ] );
11779 }
11780 }
11781
11782 if ( ! empty( $user_license_ids ) ) {
11783 foreach ( $licenses_map as $license_id => $value ) {
11784 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11785 // Associate new licenses with the user who triggered the license syncing.
11786 $user_license_ids[] = $license_id;
11787 }
11788 }
11789
11790 $user_license_ids = array_unique( $user_license_ids );
11791 } else {
11792 $user_license_ids = array_keys( $licenses_map );
11793 }
11794
11795 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11796 $user_licenses = array();
11797 foreach ( $licenses as $license ) {
11798 if ( ! in_array( $license->id, $user_license_ids ) ) {
11799 continue;
11800 }
11801
11802 $user_licenses[] = $license;
11803 }
11804
11805 $this->_licenses = $user_licenses;
11806 } else {
11807 $this->_licenses = $licenses;
11808 }
11809
11810 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11811
11812 $this->_store_licenses( true, $this->_module_id, $licenses );
11813 }
11814
11815 // Update current license.
11816 if ( is_object( $this->_license ) ) {
11817 $license = $this->_get_license_by_id( $this->_license->id );
11818
11819 if ( is_object( $license ) ) {
11820 /**
11821 * `$license` can be `false` in case a user change action has just been completed and this method
11822 * has synced the `$this->_licenses` collection for the new user. In this case, the
11823 * `$this->_licenses` collection may have only the newly activated license that is associated with
11824 * the new user. `set_license` will eventually be called in the same request by the logic that
11825 * follows outside this method which will detect that the install's license has been updated, and
11826 * then `_update_site_license` will be called which in turn will call `set_license`.
11827 *
11828 * @author Leo Fajardo (@leorw)
11829 * @since 2.3.2
11830 */
11831 $this->set_license( $license );
11832 }
11833 }
11834
11835 return $this->_licenses;
11836 }
11837
11838 /**
11839 * @author Vova Feldman (@svovaf)
11840 * @since 1.0.5
11841 *
11842 * @param number $id
11843 * @param bool $sync_licenses
11844 *
11845 * @return FS_Plugin_License|false
11846 */
11847 function _get_license_by_id( $id, $sync_licenses = true ) {
11848 $this->_logger->entrance();
11849
11850 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11851 return false;
11852 }
11853
11854 /**
11855 * When running from the network level admin and opted-in from the network,
11856 * check if the license exists in the network user licenses collection.
11857 *
11858 * @author Vova Feldman (@svovaf)
11859 * @since 2.0.0
11860 */
11861 if ( fs_is_network_admin() &&
11862 $this->is_network_registered() &&
11863 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11864 ) {
11865 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11866
11867 foreach ( $licenses as $license ) {
11868 if ( $id == $license->id ) {
11869 return $license;
11870 }
11871 }
11872 }
11873
11874 if ( ! $this->has_any_license() && $sync_licenses ) {
11875 $this->_sync_licenses( $id );
11876 }
11877
11878 if ( is_array( $this->_licenses ) ) {
11879 foreach ( $this->_licenses as $license ) {
11880 if ( $id == $license->id ) {
11881 return $license;
11882 }
11883 }
11884 }
11885
11886 return false;
11887 }
11888
11889 /**
11890 * 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.
11891 *
11892 * @author Vova Feldman (@svovaf)
11893 * @since 2.0.0
11894 *
11895 * @param number $id
11896 *
11897 * @return FS_Plugin_License
11898 */
11899 private function get_license_by_id( $id ) {
11900 $licenses = self::get_all_licenses( $this->_module_id );
11901
11902 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11903 foreach ( $licenses as $license ) {
11904 if ( $id == $license->id ) {
11905 return $license;
11906 }
11907 }
11908 }
11909
11910 return null;
11911 }
11912
11913 /**
11914 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11915 *
11916 * @author Vova Feldman (@svovaf)
11917 * @since 2.0.0
11918 *
11919 * @return \FS_Plugin_License|mixed
11920 */
11921 private function sync_site_license() {
11922 $api = $this->get_api_user_scope();
11923
11924 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11925
11926 if ( ! $this->is_api_result_entity( $result ) ) {
11927 return $result;
11928 }
11929
11930 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11931 $this->_store_licenses();
11932
11933 return $license;
11934 }
11935
11936 /**
11937 * Get all user's available licenses for the current module.
11938 *
11939 * @author Vova Feldman (@svovaf)
11940 * @since 2.0.0
11941 *
11942 * @param number $user_id
11943 *
11944 * @return FS_Plugin_License[]
11945 */
11946 private function get_user_licenses( $user_id ) {
11947 $all_licenses = self::get_all_licenses( $this->_module_id );
11948 if ( empty( $all_licenses ) ) {
11949 return array();
11950 }
11951
11952 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
11953 if ( empty( $user_license_ids ) ) {
11954 return array();
11955 }
11956
11957 $licenses = array();
11958 foreach ( $all_licenses as $license ) {
11959 if ( in_array( $license->id, $user_license_ids ) ) {
11960 $licenses[] = $license;
11961 }
11962 }
11963
11964 return $licenses;
11965 }
11966
11967 /**
11968 * Checks if the context license is network activated except on the given blog ID.
11969 *
11970 * @author Vova Feldman (@svovaf)
11971 * @since 2.0.0
11972 *
11973 * @param int $except_blog_id
11974 *
11975 * @return bool
11976 */
11977 private function is_license_network_active( $except_blog_id = 0 ) {
11978 $this->_logger->entrance();
11979
11980 if ( ! is_object( $this->_license ) ) {
11981 return false;
11982 }
11983
11984 $sites = self::get_sites();
11985
11986 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
11987 // There are more sites than the number of activations, so license cannot be network activated.
11988 return false;
11989 }
11990
11991 foreach ( $sites as $site ) {
11992 $blog_id = self::get_site_blog_id( $site );
11993
11994 if ( $except_blog_id == $blog_id ) {
11995 // Skip excluded blog.
11996 continue;
11997 }
11998
11999 $install = $this->get_install_by_blog_id( $blog_id );
12000
12001 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12002 return false;
12003 }
12004 }
12005
12006 return true;
12007 }
12008
12009 /**
12010 * 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.
12011 *
12012 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12013 *
12014 * @author Vova Feldman (@svovaf)
12015 * @since 2.0.0
12016 *
12017 * @param \FS_User $user
12018 * @param \FS_Plugin_License $license
12019 *
12020 * @return bool
12021 */
12022 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12023 $this->_logger->entrance();
12024
12025 $result = $this->can_activate_license_on_network( $license );
12026
12027 if ( false === $result ) {
12028 return false;
12029 }
12030
12031 $installs_without_license = $result['installs'];
12032 if ( ! empty( $installs_without_license ) ) {
12033 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12034 }
12035
12036 $disconnected_site_ids = $result['sites'];
12037 if ( ! empty( $disconnected_site_ids ) ) {
12038 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12039 }
12040
12041 $this->link_license_2_user( $license->id, $user->id );
12042
12043 // Sync license after activations.
12044 $license->activated += $result['production_count'];
12045 $license->activated_local += $result['localhost_count'];
12046
12047 // $this->_store_licenses()
12048
12049 return true;
12050 }
12051
12052 /**
12053 * Checks if the given license can be activated on the whole network.
12054 *
12055 * @author Vova Feldman (@svovaf)
12056 * @since 2.0.0
12057 *
12058 * @param \FS_Plugin_License $license
12059 *
12060 * @return false|array {
12061 * @type array[int]FS_Site $installs Blog ID to install map.
12062 * @type int[] $sites Non-connected blog IDs.
12063 * @type int $production_count Production sites count.
12064 * @type int $localhost_count Production sites count.
12065 * }
12066 */
12067 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12068 $sites = self::get_sites();
12069
12070 $production_count = 0;
12071 $localhost_count = 0;
12072
12073 $installs_without_license = array();
12074 $disconnected_site_ids = array();
12075
12076 foreach ( $sites as $site ) {
12077 $blog_id = self::get_site_blog_id( $site );
12078 $install = $this->get_install_by_blog_id( $blog_id );
12079
12080 if ( is_object( $install ) ) {
12081 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12082 // License already activated on the install.
12083 continue;
12084 }
12085
12086 $url = $install->url;
12087
12088 $installs_without_license[ $blog_id ] = $install;
12089 } else {
12090 $url = is_object( $site ) ?
12091 $site->siteurl :
12092 get_site_url( $blog_id );
12093
12094 $disconnected_site_ids[] = $blog_id;
12095 }
12096
12097 if ( FS_Site::is_localhost_by_address( $url ) ) {
12098 $localhost_count ++;
12099 } else {
12100 $production_count ++;
12101 }
12102 }
12103
12104 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12105 return false;
12106 }
12107
12108 return array(
12109 'installs' => $installs_without_license,
12110 'sites' => $disconnected_site_ids,
12111 'production_count' => $production_count,
12112 'localhost_count' => $localhost_count,
12113 );
12114 }
12115
12116 /**
12117 * Activate a given license on a collection of installs.
12118 *
12119 * @author Vova Feldman (@svovaf)
12120 * @since 2.0.0
12121 *
12122 * @param \FS_User $user
12123 * @param string $license_key
12124 * @param array $blog_2_install_map {
12125 * @key int Blog ID.
12126 * @value FS_Site Blog's associated install.
12127 * }
12128 *
12129 * @return mixed|true
12130 */
12131 private function activate_license_on_many_installs(
12132 FS_User $user,
12133 $license_key,
12134 array $blog_2_install_map
12135 ) {
12136 $params = array(
12137 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12138 );
12139
12140 $install_2_blog_map = array();
12141 foreach ( $blog_2_install_map as $blog_id => $install ) {
12142 $params[] = array( 'id' => $install->id );
12143
12144 $install_2_blog_map[ $install->id ] = $blog_id;
12145 }
12146
12147 $result = $this->get_api_user_scope_by_user( $user )->call(
12148 "plugins/{$this->_plugin->id}/installs.json",
12149 'PUT',
12150 $params
12151 );
12152
12153 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12154 return $result;
12155 }
12156
12157 foreach ( $result->installs as $r_install ) {
12158 $install = new FS_Site( $r_install );
12159 $install->is_disconnected = false;
12160
12161 // Update install.
12162 $this->_store_site(
12163 true,
12164 $install_2_blog_map[ $r_install->id ],
12165 $install
12166 );
12167 }
12168
12169 return true;
12170 }
12171
12172 /**
12173 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12174 *
12175 * @author Vova Feldman (@svovaf)
12176 * @since 2.3.1
12177 *
12178 * @param \FS_User $user
12179 * @param string $license_key
12180 *
12181 * @return true|mixed True if successful, otherwise, the API result.
12182 */
12183 private function activate_license_on_site( FS_User $user, $license_key ) {
12184 return $this->activate_license_on_many_sites( $user, $license_key );
12185 }
12186
12187 /**
12188 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12189 *
12190 * @author Vova Feldman (@svovaf)
12191 * @since 2.0.0
12192 *
12193 * @param \FS_User $user
12194 * @param string $license_key
12195 * @param int[] $site_ids
12196 *
12197 * @return true|mixed True if successful, otherwise, the API result.
12198 */
12199 private function activate_license_on_many_sites(
12200 FS_User $user,
12201 $license_key,
12202 array $site_ids = array()
12203 ) {
12204 $sites = array();
12205 foreach ( $site_ids as $site_id ) {
12206 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12207 }
12208
12209 // Install the plugin.
12210 $result = $this->create_installs_with_user(
12211 $user,
12212 $license_key,
12213 false,
12214 $sites,
12215 false,
12216 true
12217 );
12218
12219 if ( ! $this->is_api_result_entity( $result ) &&
12220 ! $this->is_api_result_object( $result, 'installs' )
12221 ) {
12222 return $result;
12223 }
12224
12225 $installs = array();
12226
12227 if ( $this->is_api_result_entity( $result ) ) {
12228 $install = new FS_Site( $result );
12229
12230 $this->_user = $user;
12231
12232 $this->_store_site( true, null, $install );
12233
12234 $this->_site = $install;
12235
12236 $this->reset_anonymous_mode();
12237 } else {
12238 foreach ( $result->installs as $install ) {
12239 $installs[] = new FS_Site( $install );
12240 }
12241
12242 // Map site addresses to their blog IDs.
12243 $address_to_blog_map = $this->get_address_to_blog_map();
12244
12245 $first_blog_id = null;
12246
12247 foreach ( $installs as $install ) {
12248 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12249 $blog_id = $address_to_blog_map[ $address ];
12250
12251 $this->_store_site( true, $blog_id, $install );
12252
12253 $this->reset_anonymous_mode( $blog_id );
12254
12255 if ( is_null( $first_blog_id ) ) {
12256 $first_blog_id = $blog_id;
12257 }
12258 }
12259
12260 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12261 $this->_storage->network_install_blog_id = $first_blog_id;
12262 }
12263 }
12264
12265 return true;
12266 }
12267
12268 /**
12269 * Sync site's license with user licenses.
12270 *
12271 * @author Vova Feldman (@svovaf)
12272 * @since 1.0.6
12273 *
12274 * @param FS_Plugin_License|null $new_license
12275 *
12276 * @return FS_Plugin_License|null
12277 */
12278 function _update_site_license( $new_license ) {
12279 $this->_logger->entrance();
12280
12281 /**
12282 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12283 * accordingly so that it will also handle the case when an ownership change is done via license
12284 * activation.
12285 *
12286 * @author Leo Fajardo (@leorw)
12287 * @since 2.3.2
12288 */
12289 $this->set_license( $new_license );
12290
12291 if ( ! is_object( $new_license ) ) {
12292 $this->_site->license_id = null;
12293 $this->_sync_site_subscription( null );
12294
12295 return $this->_license;
12296 }
12297
12298 $this->_site->license_id = $this->_license->id;
12299
12300 if ( ! is_array( $this->_licenses ) ) {
12301 $this->_licenses = array();
12302 }
12303
12304 $is_license_found = false;
12305 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12306 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12307 $this->_licenses[ $i ] = $new_license;
12308
12309 $is_license_found = true;
12310 break;
12311 }
12312 }
12313
12314 // If new license just append.
12315 if ( ! $is_license_found ) {
12316 $this->_licenses[] = $new_license;
12317 }
12318
12319 $this->_sync_site_subscription( $new_license );
12320
12321 return $this->_license;
12322 }
12323
12324 /**
12325 * @author Vova Feldman (@svovaf)
12326 * @since 2.3.1
12327 *
12328 * @param \FS_Plugin_License $license
12329 */
12330 private function set_license( FS_Plugin_License $license = null ) {
12331 $this->_license = $license;
12332
12333 $this->maybe_update_whitelabel_flag( $license );
12334 }
12335
12336 /**
12337 * @author Leo Fajardo (@leorw)
12338 * @since 2.3.1
12339 *
12340 * @param FS_Plugin_License $license
12341 */
12342 private function maybe_update_whitelabel_flag( $license ) {
12343 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12344 $this->_storage->is_whitelabeled :
12345 false;
12346
12347 if ( is_object( $license ) ) {
12348 $license_user = self::_get_user_by_id( $license->user_id );
12349
12350 if ( ! is_object( $license_user ) ) {
12351 // If foreign license, do not update the `is_whitelabeled` flag.
12352 return;
12353 }
12354
12355 if ( $this->is_addon() ) {
12356 /**
12357 * Store the last license data to the parent's storage since it's needed only when showing the
12358 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12359 * iterate over the add-ons just to get the last license data.
12360 */
12361 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12362 } else {
12363 $this->store_last_activated_license_data( $license );
12364 }
12365
12366 if ( $license->is_whitelabeled ) {
12367 // Activated a developer license, data should be hidden.
12368 $is_whitelabeled = true;
12369 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12370 // The account owner activated a regular license key, no need to hide the data.
12371 $is_whitelabeled = false;
12372 }
12373 }
12374
12375 $this->_storage->is_whitelabeled = $is_whitelabeled;
12376
12377 // Reset the whitelabeled status after update.
12378 $this->is_whitelabeled = null;
12379 if ( $this->is_addon() ) {
12380 $parent_fs = $this->get_parent_instance();
12381
12382 if ( is_object( $parent_fs ) ) {
12383 $parent_fs->is_whitelabeled = null;
12384 }
12385 }
12386 }
12387
12388 /**
12389 * @author Leo Fajardo (@leorw)
12390 * @since 2.3.1
12391 *
12392 * @param FS_Plugin_License $license
12393 * @param FS_User $license_user
12394 */
12395 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12396 if ( ! is_object( $license_user ) ) {
12397 $this->_storage->last_license_key = md5( $license->secret_key );
12398 $this->_storage->last_license_user_id = null;
12399 } else {
12400 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12401 $this->_storage->last_license_user_id = $license_user->id;
12402 }
12403 }
12404
12405 /**
12406 * @author Leo Fajardo (@leorw)
12407 * @since 2.3.1
12408 *
12409 * @param bool $ignore_data_debug_mode
12410 *
12411 * @return bool
12412 */
12413 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12414 if ( true !== $this->_storage->is_whitelabeled ) {
12415 return false;
12416 } else if ( $ignore_data_debug_mode ) {
12417 return true;
12418 }
12419
12420 $fs = $this->is_addon() ?
12421 $this->get_parent_instance() :
12422 $this;
12423
12424 return ! $fs->is_data_debug_mode();
12425 }
12426
12427 /**
12428 * @author Leo Fajardo (@leorw)
12429 * @since 2.3.1
12430 *
12431 * @return number
12432 */
12433 function get_last_license_user_id() {
12434 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12435 $this->_storage->last_license_user_id :
12436 null;
12437 }
12438
12439 /**
12440 * @author Leo Fajardo (@leorw)
12441 * @since 2.3.1
12442 *
12443 * @param int $blog_id
12444 * @param bool $ignore_data_debug_mode
12445 *
12446 * @return bool
12447 */
12448 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12449 if ( ! is_null( $blog_id ) ) {
12450 $this->switch_to_blog( $blog_id );
12451 }
12452
12453 if ( ! is_null( $this->is_whitelabeled ) ) {
12454 $is_whitelabeled = $this->is_whitelabeled;
12455 } else {
12456 $is_whitelabeled = false;
12457
12458 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12459
12460 if ( ! $this->has_addons() ) {
12461 $is_whitelabeled = $is_whitelabeled_flag;
12462 } else if ( $is_whitelabeled_flag ) {
12463 $is_whitelabeled = true;
12464 } else {
12465 $addon_ids = $this->get_updated_account_addons();
12466 $installed_addons = $this->get_installed_addons();
12467 foreach ( $installed_addons as $fs_addon ) {
12468 $addon_ids[] = $fs_addon->get_id();
12469 }
12470
12471 if ( ! empty( $addon_ids ) ) {
12472 $addon_ids = array_unique( $addon_ids );
12473
12474 $is_network_level = (
12475 fs_is_network_admin() &&
12476 $this->is_network_active()
12477 );
12478
12479 foreach ( $addon_ids as $addon_id ) {
12480 $addon = $this->get_addon( $addon_id );
12481
12482 if ( ! is_object( $addon ) ) {
12483 continue;
12484 }
12485
12486 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12487 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12488 self::get_addon_instance( $addon_id ) :
12489 null;
12490
12491 $was_addon_network_activated = false;
12492
12493 if ( is_object( $fs_addon ) ) {
12494 $was_addon_network_activated = $fs_addon->is_network_active();
12495 } else if ( $is_network_level ) {
12496 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12497 }
12498
12499 $network_delegated_connection = (
12500 $was_addon_network_activated &&
12501 $addon_storage->get( 'is_delegated_connection', false, true )
12502 );
12503
12504 if (
12505 $is_network_level &&
12506 ( ! $was_addon_network_activated || $network_delegated_connection )
12507 ) {
12508 $sites = self::get_sites();
12509
12510 /**
12511 * If in network admin area and the add-on was not network-activated or network-activated
12512 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12513 */
12514 foreach ( $sites as $site ) {
12515 $site_info = $this->get_site_info( $site );
12516
12517 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12518 $is_whitelabeled = true;
12519 break;
12520 }
12521 }
12522
12523 if ( $is_whitelabeled ) {
12524 break;
12525 }
12526 } else {
12527 /**
12528 * This will be executed when any of the following is met:
12529 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12530 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12531 * 3. Add-on was not network-activated and in site admin area.
12532 */
12533 if ( true === $addon_storage->is_whitelabeled ) {
12534 $is_whitelabeled = true;
12535 break;
12536 }
12537 }
12538 }
12539 }
12540 }
12541
12542 $this->is_whitelabeled = $is_whitelabeled;
12543
12544 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12545 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12546 }
12547
12548 if ( ! is_null( $blog_id ) ) {
12549 $this->restore_current_blog();
12550 }
12551 }
12552
12553 return (
12554 $is_whitelabeled &&
12555 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12556 );
12557 }
12558
12559 /**
12560 * Sync site's subscription.
12561 *
12562 * @author Vova Feldman (@svovaf)
12563 * @since 1.0.9
12564 *
12565 * @param FS_Plugin_License|null $license
12566 *
12567 * @return bool|\FS_Subscription
12568 */
12569 private function _sync_site_subscription( $license ) {
12570 if ( ! is_object( $license ) ) {
12571 $this->delete_unused_subscriptions();
12572
12573 return false;
12574 }
12575
12576 // Load subscription details if not lifetime.
12577 $subscription = $license->is_lifetime() ?
12578 false :
12579 $this->_fetch_site_license_subscription();
12580
12581 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12582 $this->store_subscription( $subscription );
12583 } else {
12584 $this->delete_unused_subscriptions();
12585 }
12586
12587 return $subscription;
12588 }
12589
12590 /**
12591 * @author Vova Feldman (@svovaf)
12592 * @since 1.0.6
12593 *
12594 * @return bool|\FS_Plugin_License
12595 */
12596 function _get_license() {
12597 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12598 return $this->_license;
12599 }
12600
12601 return $this->_get_available_premium_license();
12602 }
12603
12604 /**
12605 * @param number $license_id
12606 *
12607 * @return null|\FS_Subscription
12608 */
12609 function _get_subscription( $license_id ) {
12610 if ( ! isset( $this->_storage->subscriptions ) ||
12611 empty( $this->_storage->subscriptions )
12612 ) {
12613 return null;
12614 }
12615
12616 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12617 if ( $subscription->license_id == $license_id ) {
12618 return $subscription;
12619 }
12620 }
12621
12622 return null;
12623 }
12624
12625 /**
12626 * @author Leo Fajardo (@leorw)
12627 * @since 2.0.0
12628 *
12629 * @param FS_Subscription $subscription
12630 */
12631 function store_subscription( FS_Subscription $subscription ) {
12632 if ( ! isset( $this->_storage->subscriptions ) ) {
12633 $this->_storage->subscriptions = array();
12634 }
12635
12636 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12637 $this->_storage->subscriptions = array( $subscription );
12638
12639 return;
12640 }
12641
12642 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12643
12644 $updated_subscription = false;
12645 foreach ( $subscriptions as $key => $existing_subscription ) {
12646 if ( $existing_subscription->id == $subscription->id ) {
12647 $subscriptions[ $key ] = $subscription;
12648 $updated_subscription = true;
12649 break;
12650 }
12651 }
12652
12653 if ( ! $updated_subscription ) {
12654 $subscriptions[] = $subscription;
12655 }
12656
12657 $this->_storage->subscriptions = $subscriptions;
12658 }
12659
12660 /**
12661 * @author Leo Fajardo (@leorw)
12662 * @since 2.0.0
12663 */
12664 function delete_unused_subscriptions() {
12665 if ( ! isset( $this->_storage->subscriptions ) ||
12666 empty( $this->_storage->subscriptions ) ||
12667 // Clean up only if there are already at least 3 subscriptions.
12668 ( count( $this->_storage->subscriptions ) < 3 )
12669 ) {
12670 return;
12671 }
12672
12673 if ( ! is_multisite() ) {
12674 // If not multisite, there should only be 1 subscription, so just clear the array.
12675 $this->_storage->subscriptions = array();
12676
12677 return;
12678 }
12679
12680 $subscriptions_to_keep_by_license_id_map = array();
12681 $sites = self::get_sites();
12682 foreach ( $sites as $site ) {
12683 $blog_id = self::get_site_blog_id( $site );
12684 $install = $this->get_install_by_blog_id( $blog_id );
12685
12686 if ( ! is_object( $install ) ||
12687 ! FS_Plugin_License::is_valid_id( $install->license_id )
12688 ) {
12689 continue;
12690 }
12691
12692 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12693 }
12694
12695 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12696 $this->_storage->subscriptions = array();
12697
12698 return;
12699 }
12700
12701 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12702 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12703 unset( $this->_storage->subscriptions[ $key ] );
12704 }
12705 }
12706 }
12707
12708 /**
12709 * @author Vova Feldman (@svovaf)
12710 * @since 1.0.2
12711 *
12712 * @param string $plan Plan name
12713 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12714 *
12715 * @return bool
12716 */
12717 function is_plan( $plan, $exact = false ) {
12718 $this->_logger->entrance();
12719
12720 if ( ! $this->is_registered() ) {
12721 return false;
12722 }
12723
12724 $plan = strtolower( $plan );
12725
12726 $current_plan_name = $this->get_plan_name();
12727
12728 if ( $current_plan_name === $plan ) {
12729 // Exact plan.
12730 return true;
12731 } else if ( $exact ) {
12732 // Required exact, but plans are different.
12733 return false;
12734 }
12735
12736 $current_plan_order = - 1;
12737 $required_plan_order = PHP_INT_MAX;
12738 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12739 if ( $plan === $this->_plans[ $i ]->name ) {
12740 $required_plan_order = $i;
12741 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12742 $current_plan_order = $i;
12743 }
12744 }
12745
12746 return ( $current_plan_order > $required_plan_order );
12747 }
12748
12749 /**
12750 * Check if module has only one plan.
12751 *
12752 * @author Vova Feldman (@svovaf)
12753 * @since 1.2.1.7
12754 *
12755 * @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.
12756 *
12757 * @return bool
12758 */
12759 function is_single_plan( $double_check = false ) {
12760 $this->_logger->entrance();
12761
12762 if ( ! $this->is_registered() ||
12763 ! is_array( $this->_plans ) ||
12764 0 === count( $this->_plans )
12765 ) {
12766 return true;
12767 }
12768
12769 $has_free_plan = $this->has_free_plan();
12770
12771 if ( ! $has_free_plan && $double_check ) {
12772 foreach ( $this->_plans as $plan ) {
12773 if ( $plan->is_free() ) {
12774 $has_free_plan = true;
12775 break;
12776 }
12777 }
12778 }
12779
12780 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12781 }
12782
12783 /**
12784 * Check if plan based on trial. If not in trial mode, should return false.
12785 *
12786 * @since 1.0.9
12787 *
12788 * @param string $plan Plan name
12789 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12790 *
12791 * @return bool
12792 */
12793 function is_trial_plan( $plan, $exact = false ) {
12794 $this->_logger->entrance();
12795
12796 if ( ! $this->is_registered() ) {
12797 return false;
12798 }
12799
12800 if ( ! $this->is_trial() ) {
12801 return false;
12802 }
12803
12804 $trial_plan = $this->get_trial_plan();
12805
12806 if ( $trial_plan->name === $plan ) {
12807 // Exact plan.
12808 return true;
12809 } else if ( $exact ) {
12810 // Required exact, but plans are different.
12811 return false;
12812 }
12813
12814 $current_plan_order = - 1;
12815 $required_plan_order = - 1;
12816 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12817 if ( $plan === $this->_plans[ $i ]->name ) {
12818 $required_plan_order = $i;
12819 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12820 $current_plan_order = $i;
12821 }
12822 }
12823
12824 return ( $current_plan_order > $required_plan_order );
12825 }
12826
12827 /**
12828 * Check if plugin has any paid plans.
12829 *
12830 * @author Vova Feldman (@svovaf)
12831 * @since 1.0.7
12832 *
12833 * @return bool
12834 */
12835 function has_paid_plan() {
12836 return $this->_has_paid_plans ||
12837 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12838 }
12839
12840 /**
12841 * Check if plugin has any plan with a trail.
12842 *
12843 * @author Vova Feldman (@svovaf)
12844 * @since 1.0.9
12845 *
12846 * @return bool
12847 */
12848 function has_trial_plan() {
12849 /**
12850 * @author Vova Feldman(@svovaf)
12851 * @since 1.2.1.5
12852 *
12853 * Allow setting a trial from the SDK without calling the API.
12854 * But, if the user did opt-in, continue using the real data from the API.
12855 */
12856 if ( $this->_trial_days >= 0 ) {
12857 return true;
12858 }
12859
12860 return $this->_storage->get( 'has_trial_plan', false );
12861 }
12862
12863 /**
12864 * Check if plugin has any free plan, or is it premium only.
12865 *
12866 * Note: If no plans configured, assume plugin is free.
12867 *
12868 * @author Vova Feldman (@svovaf)
12869 * @since 1.0.7
12870 *
12871 * @return bool
12872 */
12873 function has_free_plan() {
12874 return ! $this->is_only_premium();
12875 }
12876
12877 /**
12878 * Displays a license activation dialog box when the user clicks on the "Activate License"
12879 * or "Change License" link on the plugins
12880 * page.
12881 *
12882 * @author Leo Fajardo (@leorw)
12883 * @since 1.1.9
12884 */
12885 function _add_license_activation_dialog_box() {
12886 $vars = array(
12887 'id' => $this->_module_id,
12888 );
12889
12890 fs_require_template( 'forms/license-activation.php', $vars );
12891 fs_require_template( 'forms/resend-key.php', $vars );
12892 }
12893
12894 /**
12895 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
12896 *
12897 * @author Leo Fajardo (@leorw)
12898 * @since 2.3.2
12899 *
12900 * @return number[]
12901 */
12902 function get_installs_ids_with_foreign_licenses() {
12903 $installs = array();
12904
12905 if (
12906 is_object( $this->_license ) &&
12907 $this->_site->user_id != $this->_license->user_id
12908 ) {
12909 $installs[] = $this->_site->id;
12910 }
12911
12912 /**
12913 * Also try to get foreign licenses for the context product's add-ons.
12914 */
12915 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
12916
12917 foreach ( $installs_by_slug_map as $slug => $install_info ) {
12918 if ( $slug == $this->get_slug() ) {
12919 continue;
12920 }
12921
12922 $install = $install_info['install'];
12923 $license = $install_info['license'];
12924
12925 if (
12926 is_object( $license ) &&
12927 $install->user_id != $license->user_id
12928 ) {
12929 $installs[] = $install->id;
12930 }
12931 }
12932
12933 return $installs;
12934 }
12935
12936 /**
12937 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
12938 *
12939 * @author Leo Fajardo (@leorw)
12940 * @since 2.3.2
12941 *
12942 * @param number[] $install_ids
12943 */
12944 function _add_user_change_dialog_box( $install_ids ) {
12945 $vars = array(
12946 'id' => $this->_module_id,
12947 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
12948 );
12949
12950 fs_require_template( 'forms/user-change.php', $vars );
12951 }
12952
12953 /**
12954 * @author Leo Fajardo (@leorw)
12955 * @since 2.3.1
12956 */
12957 function _add_data_debug_mode_dialog_box() {
12958 $vars = array(
12959 'id' => $this->_module_id,
12960 );
12961
12962 fs_require_template( 'forms/data-debug-mode.php', $vars );
12963 }
12964
12965 /**
12966 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
12967 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
12968 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
12969 * is only activated on a single production site.
12970 *
12971 * @author Leo Fajardo (@leorw)
12972 * @since 2.2.1
12973 *
12974 * @param bool $is_license_deactivation
12975 *
12976 * @return array
12977 */
12978 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
12979 if ( fs_is_network_admin() ) {
12980 // Subscription cancellation dialog box is currently not supported for multisite networks.
12981 return array();
12982 }
12983
12984 if ( $this->is_whitelabeled() ) {
12985 return array();
12986 }
12987
12988 $license = $this->_get_license();
12989
12990 /**
12991 * 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.
12992 *
12993 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
12994 * @since 2.2.1
12995 */
12996 if ( ! is_object( $license ) ||
12997 $license->is_lifetime() ||
12998 ( ! $license->is_single_site() && $license->activated > 1 )
12999 ) {
13000 return array();
13001 }
13002
13003 /**
13004 * @var FS_Subscription $subscription
13005 */
13006 $subscription = $this->_get_subscription( $license->id );
13007 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13008 return array();
13009 }
13010
13011 return array(
13012 'id' => $this->_module_id,
13013 'license' => $license,
13014 'has_trial' => $this->is_paid_trial(),
13015 'is_license_deactivation' => $is_license_deactivation,
13016 );
13017 }
13018
13019 /**
13020 * @author Leo Fajardo (@leorw)
13021 * @since 2.0.2
13022 */
13023 function _add_premium_version_upgrade_selection_dialog_box() {
13024 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13025 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13026 return;
13027 }
13028
13029 $vars = array(
13030 'id' => $this->_module_id,
13031 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13032 $modules_update->response[ $this->_plugin_basename ]->new_version :
13033 $modules_update->response[ $this->_plugin_basename ]['new_version']
13034 );
13035
13036 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13037 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13038 }
13039
13040 /**
13041 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13042 * page.
13043 *
13044 * @author Leo Fajardo (@leorw)
13045 * @since 1.2.1.5
13046 */
13047 function _add_optout_dialog() {
13048 if ( $this->is_theme() ) {
13049 $vars = null;
13050 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13051 }
13052
13053 $vars = array( 'id' => $this->_module_id );
13054 fs_require_template( 'forms/optout.php', $vars );
13055 }
13056
13057 /**
13058 * Prepare page to include all required UI and logic for the license activation dialog.
13059 *
13060 * @author Vova Feldman (@svovaf)
13061 * @since 1.2.0
13062 */
13063 function _add_license_activation() {
13064 if ( $this->is_migration() ) {
13065 return;
13066 }
13067
13068 if ( ! $this->is_user_admin() ) {
13069 // Only admins can activate a license.
13070 return;
13071 }
13072
13073 if ( ! $this->has_paid_plan() ) {
13074 // Module doesn't have any paid plans.
13075 return;
13076 }
13077
13078 if (
13079 $this->has_premium_version() &&
13080 ! $this->is_premium() &&
13081 /**
13082 * Also handle the case when an upgrade was made using the free version.
13083 *
13084 * @author Leo Fajardo (@leorw)
13085 * @since 2.3.2
13086 */
13087 ! is_object( $this->_get_license() )
13088 ) {
13089 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13090 return;
13091 }
13092
13093 // Add license activation link and AJAX request handler.
13094 if ( self::is_plugins_page() ) {
13095 $is_network_admin = fs_is_network_admin();
13096
13097 if (
13098 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13099 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13100 ) {
13101 /**
13102 * @since 1.2.0 Add license action link only on plugins page.
13103 */
13104 $this->_add_license_action_link();
13105 }
13106 }
13107
13108 // Add license activation AJAX callback.
13109 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13110
13111 // Add resend license AJAX callback.
13112 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13113 }
13114
13115 /**
13116 * Prepares page to include all required UI and logic for the "Change User" dialog.
13117 *
13118 * @author Leo Fajardo (@leorw)
13119 * @since 2.3.2
13120 */
13121 function _add_user_change_option() {
13122 if ( ! $this->should_handle_user_change() ) {
13123 return;
13124 }
13125
13126 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13127
13128 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13129 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13130 return;
13131 }
13132
13133 // Add user change AJAX handler.
13134 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13135 }
13136
13137 /**
13138 * @author Leo Fajardo (@leorw)
13139 * @since 2.3.2
13140 */
13141 function should_handle_user_change() {
13142 if ( ! $this->is_user_admin() ) {
13143 // Only admins can change user.
13144 return false;
13145 }
13146
13147 if ( $this->is_addon() ) {
13148 return false;
13149 }
13150
13151 if ( ! $this->is_registered() ) {
13152 return false;
13153 }
13154
13155 if (
13156 $this->is_network_active() &&
13157 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13158 ) {
13159 // Handle only on site-level "Account" section for now.
13160 return false;
13161 }
13162
13163 return true;
13164 }
13165
13166 /**
13167 * @author Leo Fajardo (@leorw)
13168 * @since 2.0.2
13169 */
13170 function _add_premium_version_upgrade_selection() {
13171 if ( ! $this->is_user_admin() ) {
13172 return;
13173 }
13174
13175 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13176 // This is relevant only to the free versions and premium versions without an active license.
13177 return;
13178 }
13179
13180 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13181 $this->_add_premium_version_upgrade_selection_action();
13182 }
13183 }
13184
13185 /**
13186 * @author Edgar Melkonyan
13187 * @since 2.4.1
13188 *
13189 * @throws Freemius_Exception
13190 */
13191 function _toggle_whitelabel_mode_ajax_handler() {
13192 $this->_logger->entrance();
13193
13194 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13195
13196 if ( ! $this->is_user_admin() ) {
13197 // Only for admins.
13198 self::shoot_ajax_failure();
13199 }
13200
13201 $license = $this->get_api_user_scope()->call(
13202 "/licenses/{$this->_site->license_id}.json",
13203 'put',
13204 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13205 );
13206
13207 if ( ! $this->is_api_result_entity( $license ) ) {
13208 self::shoot_ajax_failure(
13209 FS_Api::is_api_error_object( $license ) ?
13210 $license->error->message :
13211 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13212 );
13213 }
13214
13215 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13216 $this->_store_licenses();
13217
13218 $this->_sync_license();
13219
13220 if ( ! $license->is_whitelabeled ) {
13221 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13222 } else {
13223 $this->_admin_notices->add_sticky(
13224 sprintf(
13225 $this->get_text_inline(
13226 '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.',
13227 'license_whitelabeled'
13228 ),
13229 "<strong>{$this->get_plugin_title()}</strong>",
13230 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13231 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13232 ),
13233 'license_whitelabeled'
13234 );
13235 }
13236
13237 self::shoot_ajax_response( array( 'success' => true ) );
13238 }
13239
13240 /**
13241 * @author Leo Fajardo (@leorw)
13242 * @since 2.3.0
13243 */
13244 function _add_beta_mode_update_handler() {
13245 if ( ! $this->is_user_admin() ) {
13246 return;
13247 }
13248
13249 if ( ! $this->is_premium() ) {
13250 return;
13251 }
13252
13253 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13254 }
13255
13256 /**
13257 * @author Leo Fajardo (@leorw)
13258 * @since 2.3.0
13259 */
13260 function _set_beta_mode_ajax_handler() {
13261 $this->_logger->entrance();
13262
13263 $this->check_ajax_referer( 'set_beta_mode' );
13264
13265 if ( ! $this->is_user_admin() ) {
13266 // Only for admins.
13267 self::shoot_ajax_failure();
13268 }
13269
13270 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13271
13272 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13273 self::shoot_ajax_failure();
13274 }
13275
13276 $site = $this->get_api_site_scope()->call(
13277 '',
13278 'put',
13279 array(
13280 'is_beta' => ( 'true' == $is_beta ),
13281 'fields' => 'is_beta'
13282 )
13283 );
13284
13285 if ( ! $this->is_api_result_entity( $site ) ) {
13286 self::shoot_ajax_failure(
13287 FS_Api::is_api_error_object( $site ) ?
13288 $site->error->message :
13289 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13290 );
13291 }
13292
13293 $this->_site->is_beta = $site->is_beta;
13294 $this->_store_site();
13295
13296 self::shoot_ajax_response( array( 'success' => true ) );
13297 }
13298
13299 /**
13300 * License activation WP AJAX handler.
13301 *
13302 * @author Leo Fajardo (@leorw)
13303 * @since 1.1.9
13304 *
13305 * @uses Freemius::activate_license()
13306 */
13307 function _activate_license_ajax_action() {
13308 $this->_logger->entrance();
13309
13310 $this->check_ajax_referer( 'activate_license' );
13311
13312 $license_key = trim( fs_request_get( 'license_key' ) );
13313
13314 if ( empty( $license_key ) ) {
13315 exit;
13316 }
13317
13318 $sites = fs_is_network_admin() ?
13319 fs_request_get( 'sites', array(), 'post' ) :
13320 array();
13321
13322 $result = $this->activate_license(
13323 $license_key,
13324 $sites,
13325 fs_request_get_bool( 'is_marketing_allowed', null ),
13326 fs_request_get( 'blog_id', null ),
13327 fs_request_get( 'module_id', null, 'post' ),
13328 fs_request_get( 'user_id', null ),
13329 fs_request_get_bool( 'is_extensions_tracking_allowed', null )
13330 );
13331
13332 if (
13333 $result['success'] &&
13334 $this->is_bundle_license_auto_activation_enabled()
13335 ) {
13336 $license = new FS_Plugin_License();
13337 $license->secret_key = $license_key;
13338
13339 $this->maybe_activate_bundle_license( $license, $sites );
13340 }
13341
13342 echo json_encode( $result );
13343
13344 exit;
13345 }
13346
13347 /**
13348 * User change WP AJAX handler.
13349 *
13350 * @author Leo Fajardo (@leorw)
13351 * @since 2.3.2
13352 */
13353 function _user_change_ajax_action() {
13354 $this->_logger->entrance();
13355
13356 $this->check_ajax_referer( 'change_user' );
13357
13358 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13359 $new_user_id = fs_request_get( 'user_id' );
13360
13361 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13362 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13363 }
13364
13365 $params = array();
13366
13367 if ( ! empty( $new_email_address ) ) {
13368 $params['user_email'] = $new_email_address;
13369 } else {
13370 $params['user_id'] = $new_user_id;
13371 }
13372
13373 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13374 $install_ids = array();
13375
13376 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13377 $install_ids[ $slug ] = $install_info['install']->id;
13378 }
13379
13380 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13381
13382 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13383
13384 if ( FS_Api::is_api_error( $install ) ) {
13385 $error = '';
13386
13387 if ( is_object( $install ) ) {
13388 switch ( $install->error->code ) {
13389 case 'user_exist':
13390 $error = (
13391 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13392 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13393 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>' ) .
13394 sprintf(
13395 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13396 $this->get_account_url( 'change_owner', array(
13397 'state' => 'init',
13398 'candidate_email' => $new_email_address
13399 ) ),
13400 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13401 )
13402 );
13403 break;
13404 }
13405 }
13406
13407 if ( empty( $error ) ) {
13408 $error = FS_Api::is_api_error_object( $install ) ?
13409 $install->error->message :
13410 var_export( $install->error, true );
13411 }
13412
13413 self::shoot_ajax_failure( $error );
13414 } else {
13415 if (
13416 // If successful ownership change.
13417 $this->get_user()->id != $install->user_id ||
13418 ! empty( $new_email_address )
13419 ) {
13420 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13421 }
13422 }
13423
13424 self::shoot_ajax_success();
13425 }
13426
13427 /**
13428 * @author Leo Fajardo (@leorw)
13429 * @since 2.3.2.14
13430 */
13431 function starting_migration() {
13432 if ( ! empty( $this->_storage->license_migration ) ) {
13433 // Do not overwrite the data if already set.
13434 return;
13435 }
13436
13437 $this->_storage->license_migration = array(
13438 'is_migrating' => true,
13439 'start_timestamp' => time()
13440 );
13441 }
13442
13443 /**
13444 * @author Leo Fajardo (@leorw)
13445 * @since 2.3.2.14
13446 */
13447 function is_migration() {
13448 if ( $this->is_addon() ) {
13449 return $this->get_parent_instance()->is_migration();
13450 }
13451
13452 if ( empty( $this->_storage->license_migration ) ) {
13453 return false;
13454 }
13455
13456 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13457 return false;
13458 }
13459
13460 return (
13461 // Return `true` if the migration is within 5 minutes from the starting time.
13462 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13463 );
13464 }
13465
13466 /**
13467 *
13468 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13469 *
13470 * @author Vova Feldman (@svovaf)
13471 * @since 2.3.0
13472 *
13473 * @param string $license_key
13474 * @param null|bool $is_marketing_allowed
13475 * @param null|number $plugin_id
13476 * @param array $sites
13477 * @param int $blog_id
13478 *
13479 * @return array {
13480 * @var bool $success
13481 * @var string $error
13482 * @var string $next_page
13483 * }
13484 *
13485 * @uses Freemius::activate_license()
13486 */
13487 function activate_migrated_license(
13488 $license_key,
13489 $is_marketing_allowed = null,
13490 $plugin_id = null,
13491 $sites = array(),
13492 $blog_id = null
13493 ) {
13494 $this->_logger->entrance();
13495
13496 $result = $this->activate_license(
13497 $license_key,
13498 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13499 $this->get_sites_for_network_level_optin() :
13500 $sites,
13501 $is_marketing_allowed,
13502 $blog_id,
13503 $plugin_id
13504 );
13505
13506 // No need to show the sticky after license activation notice after migrating a license.
13507 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13508
13509 return $result;
13510 }
13511
13512 /**
13513 * @author Leo Fajardo (@leorw)
13514 * @since 2.3.1
13515 *
13516 * @return string
13517 */
13518 function get_pricing_js_path() {
13519 if ( ! isset( $this->_pricing_js_path ) ) {
13520 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
13521
13522 if ( empty( $pricing_js_path ) ) {
13523 global $fs_active_plugins;
13524
13525 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13526 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13527 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13528
13529 $pricing_js_path = $plugin_or_theme_root_dir
13530 . '/'
13531 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13532 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13533 . '/includes/freemius-pricing/freemius-pricing.js';
13534
13535 break;
13536 }
13537 }
13538 }
13539
13540 $this->_pricing_js_path = $pricing_js_path;
13541 }
13542
13543 return $this->_pricing_js_path;
13544 }
13545
13546 /**
13547 * @author Leo Fajardo (@leorw)
13548 * @since 2.3.1
13549 *
13550 * @return bool
13551 */
13552 function should_use_external_pricing() {
13553 if ( is_null( $this->_use_external_pricing ) ) {
13554 $pricing_js_path = $this->get_pricing_js_path();
13555
13556 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
13557 }
13558
13559 return $this->_use_external_pricing;
13560 }
13561
13562 /**
13563 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13564 *
13565 * @author Vova Feldman (@svovaf)
13566 * @since 2.2.4
13567 * @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).
13568 * @param string $license_key
13569 * @param array $sites
13570 * @param null|bool $is_marketing_allowed
13571 * @param null|int $blog_id
13572 * @param null|number $plugin_id
13573 * @param null|number $license_owner_id
13574 *
13575 * @return array {
13576 * @var bool $success
13577 * @var string $error
13578 * @var string $next_page
13579 * }
13580 */
13581 private function activate_license(
13582 $license_key,
13583 $sites = array(),
13584 $is_marketing_allowed = null,
13585 $blog_id = null,
13586 $plugin_id = null,
13587 $license_owner_id = null,
13588 $is_extensions_tracking_allowed = null
13589 ) {
13590 $this->_logger->entrance();
13591
13592 $license_key = trim( $license_key );
13593
13594 $is_network_activation_or_migration = (
13595 fs_is_network_admin() ||
13596 ( ! empty( $sites ) && $this->is_migration() )
13597 );
13598
13599 if ( ! $is_network_activation_or_migration ) {
13600 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13601 $sites = array();
13602 }
13603
13604 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13605 $this :
13606 $this->get_addon_instance( $plugin_id );
13607
13608 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
13609
13610 $error = false;
13611 $next_page = false;
13612
13613 $has_valid_blog_id = is_numeric( $blog_id );
13614
13615 $user = null;
13616
13617 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13618 /**
13619 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13620 *
13621 * @author Vova Feldman (@svovaf)
13622 */
13623 $user = $fs->get_parent_instance()->get_current_or_network_user();
13624 } else if ( $fs->is_registered() ) {
13625 $user = $fs->get_current_or_network_user();
13626 }
13627
13628 if ( $has_valid_blog_id ) {
13629 /**
13630 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13631 *
13632 * @author Leo Fajardo (@leorw)
13633 */
13634 $fs->switch_to_blog( $blog_id );
13635 }
13636
13637 if ( is_object( $user ) ) {
13638 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13639 // If no specific blog ID was provided, activate the license for all sites in the network.
13640 $blog_2_install_map = array();
13641 $site_ids = array();
13642
13643 foreach ( $sites as $site ) {
13644 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13645 continue;
13646 }
13647
13648 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13649
13650 if ( is_object( $install ) ) {
13651 $blog_2_install_map[ $site['blog_id'] ] = $install;
13652 } else {
13653 $site_ids[] = $site['blog_id'];
13654 }
13655 }
13656
13657 if ( ! empty( $blog_2_install_map ) ) {
13658 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13659
13660 if ( true !== $result ) {
13661 $error = FS_Api::is_api_error_object( $result ) ?
13662 $result->error->message :
13663 var_export( $result, true );
13664 }
13665 }
13666
13667 if ( empty( $error ) && ! empty( $site_ids ) ) {
13668 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13669
13670 if ( true !== $result ) {
13671 $error = FS_Api::is_api_error_object( $result ) ?
13672 $result->error->message :
13673 var_export( $result, true );
13674 }
13675 }
13676 } else {
13677 if ( $fs->is_registered() ) {
13678 $params = array(
13679 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13680 );
13681
13682 $install_ids = array();
13683
13684 $change_owner = FS_User::is_valid_id( $license_owner_id );
13685
13686 if ( $change_owner ) {
13687 $params['user_id'] = $license_owner_id;
13688
13689 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13690
13691 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13692 $install_ids[ $slug ] = $install_info['install']->id;
13693 }
13694
13695 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13696 }
13697
13698 $api = $fs->get_api_site_scope();
13699
13700 $install = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13701
13702 if ( FS_Api::is_api_error( $install ) ) {
13703 $error = FS_Api::is_api_error_object( $install ) ?
13704 $install->error->message :
13705 var_export( $install->error, true );
13706 } else {
13707 $fs->reconnect_locally( $has_valid_blog_id );
13708
13709 if (
13710 $change_owner &&
13711 // If successful ownership change.
13712 $fs->get_user()->id != $install->user_id
13713 ) {
13714 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13715 }
13716 }
13717 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13718 $result = $fs->activate_license_on_site( $user, $license_key );
13719
13720 if ( true !== $result ) {
13721 $error = FS_Api::is_api_error_object( $result ) ?
13722 $result->error->message :
13723 var_export( $result, true );
13724 }
13725 }
13726 }
13727
13728 if ( empty( $error ) ) {
13729 $fs->network_upgrade_mode_completed();
13730
13731 $fs->_user = $user;
13732
13733 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13734 $fs->_site = $fs->get_network_install();
13735 }
13736
13737 $fs->_sync_license( true, $has_valid_blog_id );
13738
13739 $this->maybe_sync_install_user();
13740
13741 $next_page = $fs->is_addon() ?
13742 $fs->get_parent_instance()->get_account_url() :
13743 $fs->get_after_activation_url( 'after_connect_url' );
13744 }
13745 } else {
13746 $next_page = $fs->opt_in(
13747 false,
13748 false,
13749 false,
13750 $license_key,
13751 false,
13752 false,
13753 false,
13754 $is_marketing_allowed,
13755 $sites
13756 );
13757
13758 if ( isset( $next_page->error ) ) {
13759 $error = $next_page->error;
13760 } else {
13761 if ( $is_network_activation_or_migration ) {
13762 /**
13763 * Get the list of sites that were just opted-in (and license activated).
13764 * This is an optimization for the next part below saving some DB queries.
13765 */
13766 $connected_sites = array();
13767 foreach ( $sites as $site ) {
13768 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13769 $connected_sites[ $site['blog_id'] ] = true;
13770 }
13771 }
13772
13773 $all_sites = self::get_sites();
13774 $pending_sites = array();
13775
13776 /**
13777 * 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.
13778 *
13779 * @author Vova Feldman (@svovaf)
13780 */
13781 foreach ( $all_sites as $site ) {
13782 $blog_id = self::get_site_blog_id( $site );
13783
13784 if ( isset( $connected_sites[ $blog_id ] ) ) {
13785 // Site was just connected.
13786 continue;
13787 }
13788
13789 if ( $fs->is_installed_on_site( $blog_id ) ) {
13790 // Site was already connected before.
13791 continue;
13792 }
13793
13794 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13795 // Site's connection was delegated.
13796 continue;
13797 }
13798
13799 if ( $fs->is_anonymous_site( $blog_id ) ) {
13800 // Site connection was already skipped.
13801 continue;
13802 }
13803
13804 $pending_sites[] = self::get_site_info( $site );
13805 }
13806
13807 if ( ! empty( $pending_sites ) ) {
13808 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13809 $fs->skip_connection( $pending_sites );
13810 } else {
13811 $fs->delegate_connection( $pending_sites );
13812 }
13813 }
13814 }
13815 }
13816 }
13817
13818 if ( false === $error && true === $fs->_storage->require_license_activation ) {
13819 $fs->_storage->require_license_activation = false;
13820 }
13821
13822 $result = array(
13823 'success' => ( false === $error )
13824 );
13825
13826 if ( false !== $error ) {
13827 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
13828 } else {
13829 if ( $fs->is_addon() || $fs->has_addons() ) {
13830 /**
13831 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
13832 * an updated valid user licenses collection will be fetched from the server which is used to also
13833 * update the account add-ons (add-ons the user has licenses for).
13834 *
13835 * @author Leo Fajardo (@leorw)
13836 * @since 2.2.4
13837 */
13838 $fs->purge_valid_user_licenses_cache();
13839 }
13840
13841 $result['next_page'] = $next_page;
13842 }
13843
13844 return $result;
13845 }
13846
13847 /**
13848 * @author Leo Fajardo (@leorw)
13849 * @since 2.3.2
13850 *
13851 * @return array {
13852 * @key string Product slug.
13853 * @value array {
13854 * @property FS_Site $site
13855 * @property FS_Plugin_License $license
13856 * }
13857 * }
13858 */
13859 private function get_parent_and_addons_installs_info() {
13860 $fs = $this->is_addon() ?
13861 $this->get_parent_instance() :
13862 $this;
13863
13864 $installed_addons_ids = array();
13865
13866 $installed_addons_instances = $fs->get_installed_addons();
13867 foreach ( $installed_addons_instances as $instance ) {
13868 $installed_addons_ids[] = $instance->get_id();
13869 }
13870
13871 $addons_ids = array_unique( array_merge(
13872 $installed_addons_ids,
13873 $fs->get_updated_account_addons()
13874 ) );
13875
13876 // Add parent product info.
13877 $installs_info_by_slug_map = array(
13878 $fs->get_slug() => array(
13879 'install' => $fs->get_site(),
13880 'license' => $fs->_get_license()
13881 )
13882 );
13883
13884 foreach ( $addons_ids as $addon_id ) {
13885 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
13886
13887 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
13888
13889 if ( ! $addon_info['is_connected'] ) {
13890 // Add-on is not associated with an install entity.
13891 continue;
13892 }
13893
13894 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
13895 'install' => $addon_info['site'],
13896 'license' => isset( $addon_info['license'] ) ?
13897 $addon_info['license'] :
13898 null
13899 );
13900 }
13901
13902 return $installs_info_by_slug_map;
13903 }
13904
13905 /**
13906 * @author Leo Fajardo (@leorw)
13907 * @since 1.2.3.1
13908 */
13909 function _network_activate_ajax_action() {
13910 $this->_logger->entrance();
13911
13912 $this->check_ajax_referer( 'network_activate' );
13913
13914 $plugin_id = fs_request_get( 'module_id', '', 'post' );
13915 $fs = ( $plugin_id == $this->_module_id ) ?
13916 $this :
13917 $this->get_addon_instance( $plugin_id );
13918
13919 $error = false;
13920
13921 $sites = fs_request_get( 'sites', array(), 'post' );
13922 if ( is_array( $sites ) && ! empty( $sites ) ) {
13923 $sites_by_action = array(
13924 'allow' => array(),
13925 'delegate' => array(),
13926 'skip' => array()
13927 );
13928
13929 foreach ( $sites as $site ) {
13930 $sites_by_action[ $site['action'] ][] = $site;
13931 }
13932
13933 $total_sites = count( $sites );
13934 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
13935
13936 $next_page = '';
13937
13938 $has_any_install = fs_request_get_bool( 'has_any_install' );
13939
13940 if ( $total_sites === $total_sites_to_delegate &&
13941 ! $this->is_network_upgrade_mode() &&
13942 ! $has_any_install
13943 ) {
13944 $this->delegate_connection();
13945 } else {
13946 if ( ! empty( $sites_by_action['delegate'] ) ) {
13947 $this->delegate_connection( $sites_by_action['delegate'] );
13948 }
13949
13950 if ( ! empty( $sites_by_action['skip'] ) ) {
13951 $this->skip_connection( $sites_by_action['skip'] );
13952 }
13953
13954 if ( empty( $sites_by_action['allow'] ) ) {
13955 if ( $has_any_install ) {
13956 $first_install = $fs->find_first_install();
13957
13958 if ( ! is_null( $first_install ) ) {
13959 $fs->_site = $first_install['install'];
13960 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
13961
13962 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
13963 $fs->_storage->network_user_id = $fs->_user->id;
13964 }
13965 }
13966 } else {
13967 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
13968 $next_page = $fs->opt_in(
13969 false,
13970 false,
13971 false,
13972 false,
13973 false,
13974 false,
13975 false,
13976 fs_request_get_bool( 'is_marketing_allowed', null ),
13977 $sites_by_action['allow']
13978 );
13979 } else {
13980 $next_page = $fs->install_with_user(
13981 $this->get_network_user(),
13982 false,
13983 false,
13984 false,
13985 true,
13986 $sites_by_action['allow']
13987 );
13988 }
13989
13990 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
13991 $error = $next_page->error;
13992 }
13993 }
13994 }
13995
13996 if ( empty( $next_page ) ) {
13997 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
13998 }
13999 } else {
14000 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14001 }
14002
14003 $result = array(
14004 'success' => ( false === $error )
14005 );
14006
14007 if ( false !== $error ) {
14008 $result['error'] = $error;
14009 } else {
14010 $result['next_page'] = $next_page;
14011 }
14012
14013 echo json_encode( $result );
14014
14015 exit;
14016 }
14017
14018 /**
14019 * Billing update AJAX callback.
14020 *
14021 * @author Vova Feldman (@svovaf)
14022 * @since 1.2.1.5
14023 */
14024 function _update_billing_ajax_action() {
14025 $this->_logger->entrance();
14026
14027 $this->check_ajax_referer( 'update_billing' );
14028
14029 if ( ! $this->is_user_admin() ) {
14030 // Only for admins.
14031 self::shoot_ajax_failure();
14032 }
14033
14034 $billing = fs_request_get( 'billing' );
14035
14036 $api = $this->get_api_user_scope();
14037 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14038 'plugin_id' => $this->get_parent_id(),
14039 ) ) );
14040
14041 if ( ! $this->is_api_result_entity( $result ) ) {
14042 self::shoot_ajax_failure();
14043 }
14044
14045 // Purge cached billing.
14046 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14047
14048 self::shoot_ajax_success();
14049 }
14050
14051 /**
14052 * Trial start for anonymous users (AJAX callback).
14053 *
14054 * @author Vova Feldman (@svovaf)
14055 * @since 1.2.1.5
14056 */
14057 function _start_trial_ajax_action() {
14058 $this->_logger->entrance();
14059
14060 $this->check_ajax_referer( 'start_trial' );
14061
14062 if ( ! $this->is_user_admin() ) {
14063 // Only for admins.
14064 self::shoot_ajax_failure();
14065 }
14066
14067 $trial_data = fs_request_get( 'trial' );
14068
14069 $next_page = $this->opt_in(
14070 false,
14071 false,
14072 false,
14073 false,
14074 false,
14075 $trial_data['plan_id']
14076 );
14077
14078 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14079 self::shoot_ajax_failure(
14080 isset( $next_page->error ) ?
14081 $next_page->error->message :
14082 var_export( $next_page, true )
14083 );
14084 }
14085
14086 $this->shoot_ajax_success( array(
14087 'next_page' => $next_page,
14088 ) );
14089 }
14090
14091 /**
14092 * @author Leo Fajardo (@leorw)
14093 * @since 1.2.0
14094 */
14095 function _resend_license_key_ajax_action() {
14096 $this->_logger->entrance();
14097
14098 $this->check_ajax_referer( 'resend_license_key' );
14099
14100 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14101
14102 if ( empty( $email_address ) ) {
14103 exit;
14104 }
14105
14106 $error = false;
14107
14108 $api = $this->get_api_plugin_scope();
14109 $result = $api->call( '/licenses/resend.json', 'post',
14110 array(
14111 'email' => $email_address,
14112 'url' => home_url(),
14113 )
14114 );
14115
14116 if ( is_object( $result ) && isset( $result->error ) ) {
14117 $error = $result->error;
14118
14119 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14120 $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' );
14121 } else if ( 'no_license' === $error->code ) {
14122 $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' );
14123 } else {
14124 $error = $error->message;
14125 }
14126 }
14127
14128 $licenses = array(
14129 'success' => ( false === $error )
14130 );
14131
14132 if ( false !== $error ) {
14133 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14134 }
14135
14136 echo json_encode( $licenses );
14137
14138 exit;
14139 }
14140
14141 /**
14142 * @author Vova Feldman (@svovaf)
14143 * @since 1.2.1.8
14144 *
14145 * @var string
14146 */
14147 private static $_pagenow;
14148
14149 /**
14150 * Get current page or the referer if executing a WP AJAX request.
14151 *
14152 * @author Vova Feldman (@svovaf)
14153 * @since 1.2.1.8
14154 *
14155 * @return string
14156 */
14157 static function get_current_page() {
14158 if ( ! isset( self::$_pagenow ) ) {
14159 global $pagenow;
14160 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14161 /**
14162 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14163 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14164 *
14165 * @author Leo Fajardo (@leorw)
14166 * @since 2.2.3
14167 */
14168 if ( is_network_admin() ) {
14169 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14170 } else if ( is_user_admin() ) {
14171 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14172 } else {
14173 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14174 }
14175
14176 $pagenow = $self_matches[1];
14177 $pagenow = trim( $pagenow, '/' );
14178 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14179 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14180 $pagenow = 'index.php';
14181 } else {
14182 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14183 $pagenow = strtolower( $self_matches[1] );
14184 if ( '.php' !== substr($pagenow, -4, 4) )
14185 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14186 }
14187 }
14188
14189 self::$_pagenow = $pagenow;
14190
14191 if ( self::is_ajax() &&
14192 'admin-ajax.php' === $pagenow
14193 ) {
14194 $referer = fs_get_raw_referer();
14195
14196 if ( is_string( $referer ) ) {
14197 $parts = explode( '?', $referer );
14198
14199 self::$_pagenow = basename( $parts[0] );
14200 }
14201 }
14202 }
14203
14204 return self::$_pagenow;
14205 }
14206
14207 /**
14208 * Helper method to check if user in the plugins page.
14209 *
14210 * @author Vova Feldman (@svovaf)
14211 * @since 1.2.1.5
14212 *
14213 * @return bool
14214 */
14215 static function is_plugins_page() {
14216 return ( 'plugins.php' === self::get_current_page() );
14217 }
14218
14219 /**
14220 * @author Leo Fajardo (@leorw)
14221 * @since 2.2.3
14222 *
14223 * @return bool
14224 */
14225 static function is_plugin_install_page() {
14226 return ( 'plugin-install.php' === self::get_current_page() );
14227 }
14228
14229 /**
14230 * @author Leo Fajardo (@leorw)
14231 * @since 2.0.2
14232 *
14233 * @return bool
14234 */
14235 static function is_updates_page() {
14236 return ( 'update-core.php' === self::get_current_page() );
14237 }
14238
14239 /**
14240 * Helper method to check if user in the themes page.
14241 *
14242 * @author Vova Feldman (@svovaf)
14243 * @since 1.2.2.6
14244 *
14245 * @return bool
14246 */
14247 static function is_themes_page() {
14248 return ( 'themes.php' === self::get_current_page() );
14249 }
14250
14251 #----------------------------------------------------------------------------------
14252 #region Affiliation
14253 #----------------------------------------------------------------------------------
14254
14255 /**
14256 * @author Leo Fajardo (@leorw)
14257 * @since 1.2.3
14258 *
14259 * @return bool
14260 */
14261 function has_affiliate_program() {
14262 if ( ! is_object( $this->_plugin ) ) {
14263 return false;
14264 }
14265
14266 return $this->_plugin->has_affiliate_program();
14267 }
14268
14269 /**
14270 * @author Leo Fajardo (@leorw)
14271 * @since 1.2.4
14272 */
14273 private function fetch_affiliate_terms() {
14274 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14275 $plugins_api = $this->get_api_plugin_scope();
14276 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14277
14278 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14279 return;
14280 }
14281
14282 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14283 }
14284 }
14285
14286 /**
14287 * @author Leo Fajardo (@leorw)
14288 * @since 1.2.4
14289 */
14290 private function fetch_affiliate_and_custom_terms() {
14291 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14292 $application_data = $this->_storage->affiliate_application_data;
14293 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14294
14295 $users_api = $this->get_api_user_scope();
14296 $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14297 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14298 if ( ! empty( $result->affiliates ) ) {
14299 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14300
14301 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14302 $application_data['status'] = $affiliate->status;
14303 $this->_storage->affiliate_application_data = $application_data;
14304 }
14305
14306 if ( $affiliate->is_using_custom_terms ) {
14307 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14308 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14309 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14310 }
14311 }
14312
14313 $this->affiliate = $affiliate;
14314 }
14315 }
14316 }
14317 }
14318
14319 /**
14320 * @author Leo Fajardo (@leorw)
14321 * @since 1.2.3
14322 */
14323 private function fetch_affiliate_and_terms() {
14324 $this->_logger->entrance();
14325
14326 $this->fetch_affiliate_terms();
14327 $this->fetch_affiliate_and_custom_terms();
14328 }
14329
14330 /**
14331 * @author Leo Fajardo (@leorw)
14332 * @since 1.2.3
14333 *
14334 * @return FS_Affiliate
14335 */
14336 function get_affiliate() {
14337 return $this->affiliate;
14338 }
14339
14340
14341 /**
14342 * @author Leo Fajardo (@leorw)
14343 * @since 1.2.3
14344 *
14345 * @return FS_AffiliateTerms
14346 */
14347 function get_affiliate_terms() {
14348 return is_object( $this->custom_affiliate_terms ) ?
14349 $this->custom_affiliate_terms :
14350 $this->plugin_affiliate_terms;
14351 }
14352
14353 /**
14354 * @author Leo Fajardo (@leorw)
14355 * @since 1.2.3
14356 */
14357 function _submit_affiliate_application() {
14358 $this->_logger->entrance();
14359
14360 $this->check_ajax_referer( 'submit_affiliate_application' );
14361
14362 if ( ! $this->is_user_admin() ) {
14363 // Only for admins.
14364 self::shoot_ajax_failure();
14365 }
14366
14367 $affiliate = fs_request_get( 'affiliate' );
14368
14369 if ( empty( $affiliate['promotion_methods'] ) ) {
14370 unset( $affiliate['promotion_methods'] );
14371 }
14372
14373 if ( ! empty( $affiliate['additional_domains'] ) ) {
14374 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14375 }
14376
14377 if ( ! $this->is_registered() ) {
14378 // Opt in but don't track usage.
14379 $next_page = $this->opt_in(
14380 false,
14381 false,
14382 false,
14383 false,
14384 false,
14385 false,
14386 true
14387 );
14388
14389 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14390 self::shoot_ajax_failure(
14391 isset( $next_page->error ) ?
14392 $next_page->error->message :
14393 var_export( $next_page, true )
14394 );
14395 } else if ( $this->is_pending_activation() ) {
14396 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) );
14397 }
14398 }
14399
14400 $this->fetch_affiliate_terms();
14401
14402 $api = $this->get_api_user_scope();
14403 $result = $api->call(
14404 ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14405 'post',
14406 $affiliate
14407 );
14408
14409 if ( $this->is_api_error( $result ) ) {
14410 self::shoot_ajax_failure(
14411 isset( $result->error ) ?
14412 $result->error->message :
14413 var_export( $result, true )
14414 );
14415 } else {
14416 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14417 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14418 }
14419
14420 $affiliate_application_data = array(
14421 'status' => 'pending',
14422 'stats_description' => $affiliate['stats_description'],
14423 'promotion_method_description' => $affiliate['promotion_method_description'],
14424 );
14425
14426 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14427 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14428 }
14429
14430 if ( ! empty( $affiliate['domain'] ) ) {
14431 $affiliate_application_data['domain'] = $affiliate['domain'];
14432 }
14433
14434 if ( ! empty( $affiliate['additional_domains'] ) ) {
14435 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14436 }
14437
14438 $this->_storage->affiliate_application_data = $affiliate_application_data;
14439 }
14440
14441 // Purge cached affiliate.
14442 $api->purge_cache( 'affiliate.json' );
14443
14444 self::shoot_ajax_success( $result );
14445 }
14446
14447 /**
14448 * @author Leo Fajardo (@leorw)
14449 * @since 1.2.3
14450 *
14451 * @return array|null
14452 */
14453 function get_affiliate_application_data() {
14454 if ( empty( $this->_storage->affiliate_application_data ) ) {
14455 return null;
14456 }
14457
14458 return $this->_storage->affiliate_application_data;
14459 }
14460
14461 #endregion Affiliation ------------------------------------------------------------
14462
14463 #----------------------------------------------------------------------------------
14464 #region URL Generators
14465 #----------------------------------------------------------------------------------
14466
14467 /**
14468 * Alias to pricing_url().
14469 *
14470 * @author Vova Feldman (@svovaf)
14471 * @since 1.0.2
14472 *
14473 * @uses pricing_url()
14474 *
14475 * @param string $period Billing cycle
14476 * @param bool $is_trial
14477 *
14478 * @return string
14479 */
14480 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14481 return $this->pricing_url( $period, $is_trial );
14482 }
14483
14484 /**
14485 * @author Vova Feldman (@svovaf)
14486 * @since 1.0.9
14487 *
14488 * @uses get_upgrade_url()
14489 *
14490 * @return string
14491 */
14492 function get_trial_url() {
14493 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14494 }
14495
14496 /**
14497 * @author Leo Fajardo (@leorw)
14498 * @since 2.1.4
14499 *
14500 * @param string $new_version
14501 *
14502 * @return string
14503 */
14504 function version_upgrade_checkout_link( $new_version ) {
14505 if ( ! is_object( $this->_license ) ) {
14506 $url = $this->pricing_url();
14507
14508 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14509 } else {
14510 $subscription = $this->_get_subscription( $this->_license->id );
14511
14512 $url = $this->checkout_url(
14513 is_object( $subscription ) ?
14514 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14515 WP_FS__PERIOD_LIFETIME,
14516 false,
14517 array( 'licenses' => $this->_license->quota )
14518 );
14519
14520 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14521 }
14522
14523 return sprintf(
14524 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14525 sprintf( '<a href="%s">%s</a>', $url, $purchase_license_text ),
14526 $new_version
14527 );
14528 }
14529
14530 /**
14531 * Plugin's pricing URL.
14532 *
14533 * @author Vova Feldman (@svovaf)
14534 * @since 1.0.4
14535 *
14536 * @param string $billing_cycle Billing cycle
14537 *
14538 * @param bool $is_trial
14539 *
14540 * @return string
14541 */
14542 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14543 $this->_logger->entrance();
14544
14545 $params = array(
14546 'billing_cycle' => $billing_cycle
14547 );
14548
14549 if ( $is_trial ) {
14550 $params['trial'] = 'true';
14551 }
14552
14553 $url = $this->is_addon() ?
14554 $this->_parent->addon_url( $this->_slug ) :
14555 $this->_get_admin_page_url( 'pricing', $params );
14556
14557 return $this->apply_filters( 'pricing_url', $url );
14558 }
14559
14560 /**
14561 * Checkout page URL.
14562 *
14563 * @author Vova Feldman (@svovaf)
14564 * @since 1.0.6
14565 *
14566 * @param string $billing_cycle Billing cycle
14567 * @param bool $is_trial
14568 * @param array $extra (optional) Extra parameters, override other query params.
14569 * @param bool|null $network
14570 *
14571 * @return string
14572 */
14573 function checkout_url(
14574 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14575 $is_trial = false,
14576 $extra = array(),
14577 $network = null
14578 ) {
14579 $this->_logger->entrance();
14580
14581 $params = array(
14582 'checkout' => 'true',
14583 'billing_cycle' => $billing_cycle,
14584 );
14585
14586 if ( $is_trial ) {
14587 $params['trial'] = 'true';
14588 }
14589
14590 /**
14591 * Params in extra override other params.
14592 */
14593 $params = array_merge( $params, $extra );
14594
14595 return $this->_get_admin_page_url( 'pricing', $params, $network );
14596 }
14597
14598 /**
14599 * Add-on checkout URL.
14600 *
14601 * @author Vova Feldman (@svovaf)
14602 * @since 1.1.7
14603 *
14604 * @param number $addon_id
14605 * @param number $pricing_id
14606 * @param string $billing_cycle
14607 * @param bool $is_trial
14608 * @param bool|null $network
14609 *
14610 * @return string
14611 */
14612 function addon_checkout_url(
14613 $addon_id,
14614 $pricing_id,
14615 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14616 $is_trial = false,
14617 $network = null
14618 ) {
14619 return $this->checkout_url( $billing_cycle, $is_trial, array(
14620 'plugin_id' => $addon_id,
14621 'pricing_id' => $pricing_id,
14622 ), $network );
14623 }
14624
14625 #endregion
14626
14627 #endregion ------------------------------------------------------------------
14628
14629 /**
14630 * Check if plugin has any add-ons.
14631 *
14632 * @author Vova Feldman (@svovaf)
14633 * @since 1.0.5
14634 *
14635 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14636 *
14637 * @return bool
14638 */
14639 function has_addons() {
14640 $this->_logger->entrance();
14641
14642 return $this->_has_addons;
14643 }
14644
14645 /**
14646 * Check if plugin can work in anonymous mode.
14647 *
14648 * @author Vova Feldman (@svovaf)
14649 * @since 1.0.9
14650 *
14651 * @return bool
14652 *
14653 * @deprecated Please use is_enable_anonymous() instead.
14654 */
14655 function enable_anonymous() {
14656 return $this->_enable_anonymous;
14657 }
14658
14659 /**
14660 * Check if plugin can work in anonymous mode.
14661 *
14662 * @author Vova Feldman (@svovaf)
14663 * @since 1.1.9
14664 *
14665 * @return bool
14666 */
14667 function is_enable_anonymous() {
14668 return $this->_enable_anonymous;
14669 }
14670
14671 /**
14672 * Check if plugin is premium only (no free plans).
14673 *
14674 * @author Vova Feldman (@svovaf)
14675 * @since 1.1.9
14676 *
14677 * @return bool
14678 */
14679 function is_only_premium() {
14680 return $this->_is_premium_only;
14681 }
14682
14683 /**
14684 * Checks if the plugin's type is "plugin". The other type is "theme".
14685 *
14686 * @author Leo Fajardo (@leorw)
14687 * @since 1.2.2
14688 *
14689 * @return bool
14690 */
14691 function is_plugin() {
14692 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14693 }
14694
14695 /**
14696 * @author Leo Fajardo (@leorw)
14697 * @since 1.2.2
14698 *
14699 * @return string
14700 */
14701 function get_module_type() {
14702 if ( ! isset( $this->_module_type ) ) {
14703 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14704 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14705 }
14706
14707 return $this->_module_type;
14708 }
14709
14710 /**
14711 * @author Leo Fajardo (@leorw)
14712 * @since 1.2.2
14713 *
14714 * @return string
14715 */
14716 function get_plugin_main_file_path() {
14717 return $this->_plugin_main_file_path;
14718 }
14719
14720 /**
14721 * Check if module has a premium code version.
14722 *
14723 * Serviceware module might be freemium without any
14724 * premium code version, where the paid features
14725 * are all part of the service.
14726 *
14727 * @author Vova Feldman (@svovaf)
14728 * @since 1.2.1.6
14729 *
14730 * @return bool
14731 */
14732 function has_premium_version() {
14733 return $this->_has_premium_version;
14734 }
14735
14736 /**
14737 * Check if feature supported with current site's plan.
14738 *
14739 * @author Vova Feldman (@svovaf)
14740 * @since 1.0.1
14741 *
14742 * @todo IMPLEMENT
14743 *
14744 * @param number $feature_id
14745 *
14746 * @throws Exception
14747 */
14748 function is_feature_supported( $feature_id ) {
14749 throw new Exception( 'not implemented' );
14750 }
14751
14752 /**
14753 * @author Vova Feldman (@svovaf)
14754 * @since 1.0.1
14755 *
14756 * @return bool Is running in SSL/HTTPS
14757 */
14758 function is_ssl() {
14759 return WP_FS__IS_HTTPS;
14760 }
14761
14762 /**
14763 * @author Vova Feldman (@svovaf)
14764 * @since 1.0.9
14765 *
14766 * @return bool Is running in AJAX call.
14767 *
14768 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
14769 */
14770 static function is_ajax() {
14771 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
14772 }
14773
14774 /**
14775 * Check if it's an AJAX call targeted for the current module.
14776 *
14777 * @author Vova Feldman (@svovaf)
14778 * @since 1.2.0
14779 *
14780 * @param array|string $actions Collection of AJAX actions.
14781 *
14782 * @return bool
14783 */
14784 function is_ajax_action( $actions ) {
14785 // Verify it's an ajax call.
14786 if ( ! self::is_ajax() ) {
14787 return false;
14788 }
14789
14790 // Verify the call is relevant for the plugin.
14791 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
14792 return false;
14793 }
14794
14795 // Verify it's one of the specified actions.
14796 if ( is_string( $actions ) ) {
14797 $actions = explode( ',', $actions );
14798 }
14799
14800 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14801 $ajax_action = fs_request_get( 'action' );
14802
14803 foreach ( $actions as $action ) {
14804 if ( $ajax_action === $this->get_action_tag( $action ) ) {
14805 return true;
14806 }
14807 }
14808 }
14809
14810 return false;
14811 }
14812
14813 /**
14814 * Check if it's an AJAX call targeted for current request.
14815 *
14816 * @author Vova Feldman (@svovaf)
14817 * @since 1.2.0
14818 *
14819 * @param array|string $actions Collection of AJAX actions.
14820 * @param number|null $module_id
14821 *
14822 * @return bool
14823 */
14824 static function is_ajax_action_static( $actions, $module_id = null ) {
14825 // Verify it's an ajax call.
14826 if ( ! self::is_ajax() ) {
14827 return false;
14828 }
14829
14830
14831 if ( ! empty( $module_id ) ) {
14832 // Verify the call is relevant for the plugin.
14833 if ( $module_id != fs_request_get( 'module_id' ) ) {
14834 return false;
14835 }
14836 }
14837
14838 // Verify it's one of the specified actions.
14839 if ( is_string( $actions ) ) {
14840 $actions = explode( ',', $actions );
14841 }
14842
14843 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14844 $ajax_action = fs_request_get( 'action' );
14845
14846 foreach ( $actions as $action ) {
14847 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
14848 return true;
14849 }
14850 }
14851 }
14852
14853 return false;
14854 }
14855
14856 /**
14857 * @author Vova Feldman (@svovaf)
14858 * @since 1.1.7
14859 *
14860 * @return bool
14861 */
14862 static function is_cron() {
14863 return ( defined( 'DOING_CRON' ) && DOING_CRON );
14864 }
14865
14866 /**
14867 * Check if a real user is visiting the admin dashboard.
14868 *
14869 * @author Vova Feldman (@svovaf)
14870 * @since 1.1.7
14871 *
14872 * @return bool
14873 */
14874 function is_user_in_admin() {
14875 return (
14876 is_admin() &&
14877 ! self::is_ajax() &&
14878 ! self::is_cron() &&
14879 ( 'admin-post.php' !== self::get_current_page() )
14880 );
14881 }
14882
14883 /**
14884 * Check if a real user is in the customizer view.
14885 *
14886 * @author Vova Feldman (@svovaf)
14887 * @since 1.2.2.7
14888 *
14889 * @return bool
14890 */
14891 static function is_customizer() {
14892 return is_customize_preview();
14893 }
14894
14895 /**
14896 * Check if running in HTTPS and if site's plan matching the specified plan.
14897 *
14898 * @param string $plan
14899 * @param bool $exact
14900 *
14901 * @return bool
14902 */
14903 function is_ssl_and_plan( $plan, $exact = false ) {
14904 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
14905 }
14906
14907 /**
14908 * Construct plugin's settings page URL.
14909 *
14910 * @author Vova Feldman (@svovaf)
14911 * @since 1.0.4
14912 *
14913 * @param string $page
14914 * @param array $params
14915 * @param bool|null $network
14916 *
14917 * @return string
14918 */
14919 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
14920 if ( is_null( $network ) ) {
14921 $network = (
14922 $this->_is_network_active &&
14923 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
14924 );
14925 }
14926
14927 if ( 0 < count( $params ) ) {
14928 foreach ( $params as $k => $v ) {
14929 $params[ $k ] = urlencode( $v );
14930 }
14931 }
14932
14933 $page_param = $this->_menu->get_slug( $page );
14934
14935 if ( empty( $page ) &&
14936 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
14937 $this->show_opt_in_on_themes_page()
14938 ) {
14939 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
14940
14941 return add_query_arg(
14942 $params,
14943 $this->admin_url( 'themes.php', 'admin', $network )
14944 );
14945 }
14946
14947 if ( ! $this->has_settings_menu() ) {
14948 if ( ! empty( $page ) ) {
14949 // Module doesn't have a setting page, but since the request is for
14950 // a specific Freemius page, use the admin.php path.
14951 return add_query_arg( array_merge( $params, array(
14952 'page' => $page_param,
14953 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14954 } else {
14955 if ( $this->is_activation_mode() ) {
14956 /**
14957 * @author Vova Feldman
14958 * @since 1.2.1.6
14959 *
14960 * If plugin doesn't have a settings page, create one for the opt-in screen.
14961 */
14962 return add_query_arg( array_merge( $params, array(
14963 'page' => $this->_slug,
14964 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14965 } else {
14966 // Plugin without a settings page.
14967 return add_query_arg(
14968 $params,
14969 $this->admin_url( 'plugins.php', 'admin', $network )
14970 );
14971 }
14972 }
14973 }
14974
14975 // Module has a submenu settings page.
14976 if ( ! $this->_menu->is_top_level() ) {
14977 $parent_slug = $this->_menu->get_parent_slug();
14978 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
14979 $parent_slug :
14980 'admin.php';
14981
14982 return add_query_arg( array_merge( $params, array(
14983 'page' => $page_param,
14984 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
14985 }
14986
14987 // Module has a top level CPT settings page.
14988 if ( $this->_menu->is_cpt() ) {
14989 if ( empty( $page ) && $this->is_activation_mode() ) {
14990 return add_query_arg( array_merge( $params, array(
14991 'page' => $page_param
14992 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14993 } else {
14994 if ( ! empty( $page ) ) {
14995 $params['page'] = $page_param;
14996 }
14997
14998 return add_query_arg(
14999 $params,
15000 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15001 );
15002 }
15003 }
15004
15005 // Module has a custom top level settings page.
15006 return add_query_arg( array_merge( $params, array(
15007 'page' => $page_param,
15008 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15009 }
15010
15011 #--------------------------------------------------------------------------------
15012 #region Multisite
15013 #--------------------------------------------------------------------------------
15014
15015 /**
15016 * @author Leo Fajardo (@leorw)
15017 * @since 2.0.0
15018 *
15019 * @return bool
15020 */
15021 function is_network_active() {
15022 return $this->_is_network_active;
15023 }
15024
15025 /**
15026 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15027 *
15028 * @author Leo Fajardo (@leorw)
15029 * @since 2.0.0
15030 *
15031 * @param array|null $sites
15032 */
15033 private function delegate_connection( $sites = null ) {
15034 $this->_logger->entrance();
15035
15036 $this->_admin_notices->remove_sticky( 'connect_account' );
15037
15038 if ( is_null( $sites ) ) {
15039 // All sites delegation.
15040 $this->_storage->store( 'is_delegated_connection', true, true, true );
15041 } else {
15042 // Specified sites delegation.
15043 foreach ( $sites as $site ) {
15044 $this->delegate_site_connection( $site['blog_id'] );
15045 }
15046 }
15047
15048 $this->network_upgrade_mode_completed();
15049 }
15050
15051 /**
15052 * Delegate specific network site conncetion to the site admin.
15053 *
15054 * @author Vova Feldman (@svovaf)
15055 * @since 2.0.0
15056 *
15057 * @param int $blog_id
15058 */
15059 private function delegate_site_connection( $blog_id ) {
15060 $this->_storage->store( 'is_delegated_connection', true, $blog_id, true );
15061 }
15062
15063 /**
15064 * Check if super-admin delegated the connection of ALL sites to the site admins.
15065 *
15066 * @author Vova Feldman (@svovaf)
15067 * @since 2.0.0
15068 *
15069 * @return bool
15070 */
15071 function is_network_delegated_connection() {
15072 if ( ! $this->_is_network_active ) {
15073 return false;
15074 }
15075
15076 return $this->_storage->get( 'is_delegated_connection', false, true );
15077 }
15078
15079 /**
15080 * @author Leo Fajardo (@leorw)
15081 * @since 2.0.0
15082 *
15083 * @param int $blog_id
15084 *
15085 * @return bool
15086 */
15087 function is_site_delegated_connection( $blog_id = 0 ) {
15088 if ( ! $this->_is_network_active ) {
15089 return false;
15090 }
15091
15092 if ( 0 == $blog_id ) {
15093 $blog_id = get_current_blog_id();
15094 }
15095
15096 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15097 }
15098
15099 /**
15100 * Check if delegated the connection. When running within the the network admin,
15101 * and haven't specified the blog ID, checks if network level delegated. If running
15102 * within a site admin or specified a blog ID, check if delegated the connection for
15103 * the current context site.
15104 *
15105 * If executed outside the the admin, check if delegated the connection
15106 * for the current context site OR the whole network.
15107 *
15108 * @author Vova Feldman (@svovaf)
15109 * @since 2.0.0
15110 *
15111 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15112 *
15113 * @return bool
15114 */
15115 function is_delegated_connection( $blog_id = 0 ) {
15116 if ( ! $this->_is_network_active ) {
15117 return false;
15118 }
15119
15120 if ( fs_is_network_admin() && 0 == $blog_id ) {
15121 return $this->is_network_delegated_connection();
15122 }
15123
15124 return (
15125 $this->is_network_delegated_connection() ||
15126 $this->is_site_delegated_connection( $blog_id )
15127 );
15128 }
15129
15130 /**
15131 * Check if the current module is active for the site.
15132 *
15133 * @author Vova Feldman (@svovaf)
15134 * @since 2.0.0
15135 *
15136 * @param int $blog_id
15137 *
15138 * @return bool
15139 */
15140 function is_active_for_site( $blog_id ) {
15141 if ( ! is_multisite() ) {
15142 // Not a multisite and this code is executed, means that the plugin is active.
15143 return true;
15144 }
15145
15146 if ( $this->is_theme() ) {
15147 // All themes are site level activated.
15148 return true;
15149 }
15150
15151 if ( $this->_is_network_active ) {
15152 // Plugin was network activated so it's active.
15153 return true;
15154 }
15155
15156 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15157 }
15158
15159 /**
15160 * @author Leo Fajardo (@leorw)
15161 * @since 2.0.0
15162 *
15163 * @return array Active & public sites collection.
15164 */
15165 static function get_sites() {
15166 if ( ! is_multisite() ) {
15167 return array();
15168 }
15169
15170 /**
15171 * For consistency with get_blog_list() which only return active public sites.
15172 *
15173 * @author Vova Feldman (@svovaf)
15174 */
15175 $args = array(
15176 /**
15177 * Commented out in order to handle the migration of site options whether the site is public or not.
15178 *
15179 * @author Leo Fajardo (@leorw)
15180 * @since 2.2.1
15181 */
15182 // 'public' => 1,
15183 'archived' => 0,
15184 'mature' => 0,
15185 'spam' => 0,
15186 'deleted' => 0,
15187 );
15188
15189 if ( function_exists( 'get_sites' ) ) {
15190 // For WP 4.6 and above.
15191 return get_sites( $args );
15192 } else if ( function_exists( 'wp_' . 'get_sites' ) ) {
15193 // For WP 3.7 to WP 4.5.
15194 /**
15195 * 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.
15196 *
15197 * @todo Remove this hack once this false-positive error is removed from the Theme Sniffer.
15198 *
15199 * @since 2.3.3
15200 * @author Vova Feldman (@svovaf)
15201 */
15202 $fn = 'wp_' . 'get_sites';
15203 return $fn( $args );
15204 } else {
15205 // For WP 3.6 and below.
15206 return get_blog_list( 0, 'all' );
15207 }
15208 }
15209
15210 /**
15211 * Checks if a given blog is active.
15212 *
15213 * @author Vova Feldman (@svovaf)
15214 * @since 2.0.0
15215 *
15216 * @param $blog_id
15217 *
15218 * @return bool
15219 */
15220 private static function is_site_active( $blog_id ) {
15221 global $wpdb;
15222
15223 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15224
15225 if ( ! is_object( $blog_info ) ) {
15226 return false;
15227 }
15228
15229 return (
15230 true == $blog_info->public &&
15231 false == $blog_info->archived &&
15232 false == $blog_info->mature &&
15233 false == $blog_info->spam &&
15234 false == $blog_info->deleted
15235 );
15236 }
15237
15238 /**
15239 * Get a mapping between the site addresses to their blog IDs.
15240 *
15241 * @author Vova Feldman (@svovaf)
15242 * @since 2.0.0
15243 *
15244 * @return array {
15245 * @key string Site address without protocol with a trailing slash.
15246 * @value int Site's blog ID.
15247 * }
15248 */
15249 private function get_address_to_blog_map() {
15250 $sites = self::get_sites();
15251
15252 // Map site addresses to their blog IDs.
15253 $address_to_blog_map = array();
15254 foreach ( $sites as $site ) {
15255 $blog_id = self::get_site_blog_id( $site );
15256 $address = trailingslashit( fs_strip_url_protocol( get_site_url( $blog_id ) ) );
15257 $address_to_blog_map[ $address ] = $blog_id;
15258 }
15259
15260 return $address_to_blog_map;
15261 }
15262
15263 /**
15264 * Get a mapping between the site addresses to their blog IDs.
15265 *
15266 * @author Vova Feldman (@svovaf)
15267 * @since 2.0.0
15268 *
15269 * @return array {
15270 * @key int Site's blog ID.
15271 * @value FS_Site Associated install.
15272 * }
15273 */
15274 function get_blog_install_map() {
15275 $sites = self::get_sites();
15276
15277 // Map site blog ID to its install.
15278 $install_map = array();
15279
15280 foreach ( $sites as $site ) {
15281 $blog_id = self::get_site_blog_id( $site );
15282 $install = $this->get_install_by_blog_id( $blog_id );
15283
15284 if ( is_object( $install ) ) {
15285 $install_map[ $blog_id ] = $install;
15286 }
15287 }
15288
15289 return $install_map;
15290 }
15291
15292 /**
15293 * Gets a map of module IDs that the given user has opted-in to.
15294 *
15295 * @author Leo Fajardo (@leorw)
15296 * @since 2.1.0
15297 *
15298 * @param number $fs_user_id
15299 *
15300 * @return array {
15301 * @key number $plugin_id
15302 * @value bool Always true.
15303 * }
15304 */
15305 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15306 self::$_static_logger->entrance();
15307
15308 if ( ! is_multisite() ) {
15309 $installs = array_merge(
15310 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15311 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15312 );
15313 } else {
15314 $sites = self::get_sites();
15315
15316 $installs = array();
15317 foreach ( $sites as $site ) {
15318 $blog_id = self::get_site_blog_id( $site );
15319
15320 $installs = array_merge(
15321 $installs,
15322 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15323 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15324 );
15325 }
15326 }
15327
15328 $module_ids_map = array();
15329 foreach ( $installs as $install ) {
15330 if ( is_object( $install ) &&
15331 FS_Site::is_valid_id( $install->id ) &&
15332 FS_User::is_valid_id( $install->user_id ) &&
15333 ( $install->user_id == $fs_user_id )
15334 ) {
15335 $module_ids_map[ $install->plugin_id ] = true;
15336 }
15337 }
15338
15339 return $module_ids_map;
15340 }
15341
15342 /**
15343 * @author Leo Fajardo (@leorw)
15344 *
15345 * @return null|array {
15346 * 'install' => FS_Site Module's install,
15347 * 'blog_id' => string The associated blog ID.
15348 * }
15349 */
15350 function find_first_install() {
15351 $sites = self::get_sites();
15352
15353 foreach ( $sites as $site ) {
15354 $blog_id = self::get_site_blog_id( $site );
15355 $install = $this->get_install_by_blog_id( $blog_id );
15356
15357 if ( is_object( $install ) ) {
15358 return array(
15359 'install' => $install,
15360 'blog_id' => $blog_id
15361 );
15362 }
15363 }
15364
15365 return null;
15366 }
15367
15368 /**
15369 * Switches the Freemius site level context to a specified blog.
15370 *
15371 * @author Vova Feldman (@svovaf)
15372 * @since 2.0.0
15373 *
15374 * @param int $blog_id
15375 * @param FS_Site $install
15376 *
15377 * @return bool Since 2.3.1 returns if a switch was made.
15378 */
15379 function switch_to_blog( $blog_id, FS_Site $install = null ) {
15380 if ( ! is_numeric( $blog_id ) || $blog_id == $this->_context_is_network_or_blog_id ) {
15381 return false;
15382 }
15383
15384 switch_to_blog( $blog_id );
15385 $this->_context_is_network_or_blog_id = $blog_id;
15386
15387 self::$_accounts->set_site_blog_context( $blog_id );
15388 $this->_storage->set_site_blog_context( $blog_id );
15389 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15390
15391 $this->_site = is_object( $install ) ?
15392 $install :
15393 $this->get_install_by_blog_id( $blog_id );
15394
15395 $this->_user = false;
15396 $this->_licenses = false;
15397 $this->_license = null;
15398 $this->is_whitelabeled = null;
15399
15400 if ( is_object( $this->_site ) ) {
15401 // Try to fetch user from install.
15402 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15403
15404 if ( ! is_object( $this->_user ) &&
15405 FS_User::is_valid_id( $this->_storage->prev_user_id )
15406 ) {
15407 // Try to fetch previously saved user.
15408 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15409
15410 if ( ! is_object( $this->_user ) ) {
15411 // Fallback to network's user.
15412 $this->_user = $this->get_network_user();
15413 }
15414 }
15415
15416 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15417
15418 if ( ! empty( $all_plugin_licenses ) ) {
15419 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15420 $this->_license = null;
15421 } else {
15422 $license_found = false;
15423 foreach ( $all_plugin_licenses as $license ) {
15424 if ( $license->id == $this->_site->license_id ) {
15425 // License found.
15426 $this->_license = $license;
15427 $license_found = true;
15428 break;
15429 }
15430 }
15431
15432 if ( $license_found ) {
15433 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15434 }
15435 }
15436
15437 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15438 }
15439 }
15440
15441 unset( $this->_site_api );
15442 unset( $this->_user_api );
15443
15444 return false;
15445 }
15446
15447 /**
15448 * Restore the blog context to the blog that originally loaded the module.
15449 *
15450 * @author Vova Feldman (@svovaf)
15451 * @since 2.0.0
15452 */
15453 function restore_current_blog() {
15454 $this->switch_to_blog( $this->_blog_id );
15455 }
15456
15457 /**
15458 * @author Vova Feldman (@svovaf)
15459 * @since 2.0.0
15460 *
15461 * @param array|WP_Site $site
15462 *
15463 * @return int
15464 */
15465 static function get_site_blog_id( &$site ) {
15466 return ( $site instanceof WP_Site ) ?
15467 $site->blog_id :
15468 ( is_object( $site ) && isset( $site->userblog_id ) ?
15469 $site->userblog_id :
15470 $site['blog_id'] );
15471 }
15472
15473 /**
15474 * @author Leo Fajardo (@leorw)
15475 * @since 2.0.0
15476 *
15477 * @param array|WP_Site|null $site
15478 *
15479 * @return array
15480 */
15481 function get_site_info( $site = null ) {
15482 $this->_logger->entrance();
15483
15484 $switched = false;
15485
15486 if ( is_null( $site ) ) {
15487 $url = get_site_url();
15488 $name = get_bloginfo( 'name' );
15489 $blog_id = null;
15490 } else {
15491 $blog_id = self::get_site_blog_id( $site );
15492
15493 if ( get_current_blog_id() != $blog_id ) {
15494 switch_to_blog( $blog_id );
15495 $switched = true;
15496 }
15497
15498 if ( $site instanceof WP_Site ) {
15499 $url = $site->siteurl;
15500 $name = $site->blogname;
15501 } else {
15502 $url = get_site_url( $blog_id );
15503 $name = get_bloginfo( 'name' );
15504 }
15505 }
15506
15507 $info = array(
15508 'uid' => $this->get_anonymous_id( $blog_id ),
15509 'url' => $url,
15510 'title' => $name,
15511 'language' => get_bloginfo( 'language' ),
15512 'charset' => get_bloginfo( 'charset' ),
15513 );
15514
15515 if ( is_numeric( $blog_id ) ) {
15516 $info['blog_id'] = $blog_id;
15517 }
15518
15519 if ( $switched ) {
15520 restore_current_blog();
15521 }
15522
15523 return $info;
15524 }
15525
15526 /**
15527 * Load the module's install based on the blog ID.
15528 *
15529 * @author Vova Feldman (@svovaf)
15530 * @since 2.0.0
15531 *
15532 * @param int|null $blog_id
15533 *
15534 * @return FS_Site
15535 */
15536 function get_install_by_blog_id( $blog_id = null ) {
15537 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15538 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15539
15540 if ( is_object( $install ) &&
15541 is_numeric( $install->id ) &&
15542 is_numeric( $install->user_id ) &&
15543 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15544 ) {
15545 // Load site.
15546 $install = clone $install;
15547 }
15548
15549 return $install;
15550 }
15551
15552 /**
15553 * Check if module is installed on a specified site.
15554 *
15555 * @author Vova Feldman (@svovaf)
15556 * @since 2.0.0
15557 *
15558 * @param int|null $blog_id
15559 *
15560 * @return bool
15561 */
15562 function is_installed_on_site( $blog_id = null ) {
15563 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15564 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15565
15566 return (
15567 is_object( $install ) &&
15568 is_numeric( $install->id ) &&
15569 is_numeric( $install->user_id ) &&
15570 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15571 );
15572 }
15573
15574 /**
15575 * Check if super-admin connected at least one site via the network opt-in.
15576 *
15577 * @author Vova Feldman (@svovaf)
15578 * @since 2.0.0
15579 *
15580 * @return bool
15581 */
15582 function is_network_registered() {
15583 if ( ! $this->_is_network_active ) {
15584 return false;
15585 }
15586
15587 return FS_User::is_valid_id( $this->_storage->network_user_id );
15588 }
15589
15590 /**
15591 * Returns the main user associated with the network.
15592 *
15593 * @author Vova Feldman (@svovaf)
15594 * @since 2.0.0
15595 *
15596 * @return FS_User
15597 */
15598 function get_network_user() {
15599 if ( ! $this->_is_network_active ) {
15600 return null;
15601 }
15602
15603 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15604 self::_get_user_by_id( $this->_storage->network_user_id ) :
15605 null;
15606 }
15607
15608 /**
15609 * Returns the current context user or the network's main user.
15610 *
15611 * @author Vova Feldman (@svovaf)
15612 * @since 2.0.0
15613 *
15614 * @return FS_User
15615 */
15616 function get_current_or_network_user() {
15617 return ( $this->_user instanceof FS_User ) ?
15618 $this->_user :
15619 $this->get_network_user();
15620 }
15621
15622 /**
15623 * Returns the main install associated with the network.
15624 *
15625 * @author Vova Feldman (@svovaf)
15626 * @since 2.0.0
15627 *
15628 * @return FS_Site
15629 */
15630 function get_network_install() {
15631 if ( ! $this->_is_network_active ) {
15632 return null;
15633 }
15634
15635 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15636 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15637 null;
15638 }
15639
15640 /**
15641 * Returns the blog ID that is associated with the main install.
15642 *
15643 * @author Leo Fajardo (@leorw)
15644 * @since 2.0.0
15645 *
15646 * @return int|null
15647 */
15648 function get_network_install_blog_id() {
15649 if ( ! $this->_is_network_active ) {
15650 return null;
15651 }
15652
15653 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15654 $this->_storage->network_install_blog_id :
15655 null;
15656 }
15657
15658 /**
15659 * Returns the current context install or the network's main install.
15660 *
15661 * @author Vova Feldman (@svovaf)
15662 * @since 2.0.0
15663 *
15664 * @return FS_Site
15665 */
15666 function get_current_or_network_install() {
15667 return ( $this->_site instanceof FS_Site ) ?
15668 $this->_site :
15669 $this->get_network_install();
15670 }
15671
15672 /**
15673 * Check if executing a site level action from the network level admin.
15674 *
15675 * @author Vova Feldman (@svovaf)
15676 * @since 2.0.0
15677 *
15678 * @return false|int If yes, return the requested blog ID.
15679 */
15680 private function is_network_level_site_specific_action() {
15681 if ( ! $this->_is_network_active ) {
15682 return false;
15683 }
15684
15685 if ( ! fs_is_network_admin() ) {
15686 return false;
15687 }
15688
15689 $blog_id = fs_request_get( 'blog_id', '' );
15690
15691 return is_numeric( $blog_id ) ? $blog_id : false;
15692 }
15693
15694 /**
15695 * Check if executing an action from the network level admin.
15696 *
15697 * @author Vova Feldman (@svovaf)
15698 * @since 2.0.0
15699 *
15700 * @return bool
15701 */
15702 private function is_network_level_action() {
15703 return ( $this->_is_network_active && fs_is_network_admin() );
15704 }
15705
15706 /**
15707 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
15708 * 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.
15709 *
15710 * @author Vova Feldman (@svovaf)
15711 * @since 2.0.0
15712 *
15713 * @param int $context_blog_id
15714 */
15715 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
15716 $this->_logger->entrance();
15717
15718 if ( $this->_is_network_active ) {
15719 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
15720 $installs_map = $this->get_blog_install_map();
15721
15722 foreach ( $installs_map as $blog_id => $install ) {
15723 /**
15724 * @var FS_Site $install
15725 */
15726 if ( $context_blog_id == $blog_id ) {
15727 continue;
15728 }
15729
15730 if ( $install->user_id != $this->_storage->network_user_id ) {
15731 continue;
15732 }
15733
15734 // Switch reference to a blog that is opted-in and belong to the same super-admin.
15735 $this->_storage->network_install_blog_id = $blog_id;
15736 break;
15737 }
15738 }
15739 }
15740
15741 if ( $this->is_sync_cron_scheduled() &&
15742 $context_blog_id == $this->get_sync_cron_blog_id()
15743 ) {
15744 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
15745 }
15746
15747 if ( $this->is_install_sync_scheduled() &&
15748 $context_blog_id == $this->get_install_sync_cron_blog_id()
15749 ) {
15750 $this->schedule_install_sync( $context_blog_id );
15751 }
15752 }
15753
15754 /**
15755 * Executed after site deactivation, archive, or flag as spam.
15756 *
15757 * @author Vova Feldman (@svovaf)
15758 * @since 2.0.0
15759 *
15760 * @param int $context_blog_id
15761 */
15762 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
15763 $this->_logger->entrance();
15764
15765 $install = $this->get_install_by_blog_id( $context_blog_id );
15766
15767 if ( ! is_object( $install ) ) {
15768 // Site not connected.
15769 return;
15770 }
15771
15772 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15773
15774 $current_blog_id = get_current_blog_id();
15775
15776 $this->switch_to_blog( $context_blog_id );
15777
15778 // Send deactivation event.
15779 $this->sync_install( array(
15780 'is_active' => false,
15781 ) );
15782
15783 $this->switch_to_blog( $current_blog_id );
15784 }
15785
15786 /**
15787 * Executed after site deletion.
15788 *
15789 * @author Vova Feldman (@svovaf)
15790 * @since 2.0.0
15791 *
15792 * @param int $context_blog_id
15793 * @param bool $drop True if site's database tables should be dropped. Default is false.
15794 */
15795 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
15796 $this->_logger->entrance();
15797
15798 $install = $this->get_install_by_blog_id( $context_blog_id );
15799
15800 if ( ! is_object( $install ) ) {
15801 // Site not connected.
15802 return;
15803 }
15804
15805 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15806
15807 $current_blog_id = get_current_blog_id();
15808
15809 $this->switch_to_blog( $context_blog_id );
15810
15811 if ( $drop ) {
15812 // Delete install if dropping site DB.
15813 $this->delete_account_event();
15814 } else {
15815 // Send deactivation event.
15816 $this->sync_install( array(
15817 'is_active' => false,
15818 ) );
15819 }
15820
15821 $this->switch_to_blog( $current_blog_id );
15822 }
15823
15824 /**
15825 * Executed after site re-activation.
15826 *
15827 * @author Vova Feldman (@svovaf)
15828 * @since 2.0.0
15829 *
15830 * @param int $context_blog_id
15831 */
15832 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
15833 $this->_logger->entrance();
15834
15835 $install = $this->get_install_by_blog_id( $context_blog_id );
15836
15837 if ( ! is_object( $install ) ) {
15838 // Site not connected.
15839 return;
15840 }
15841
15842 if ( ! self::is_site_active( $context_blog_id ) ) {
15843 // Site not yet active (can be in spam mode, archived, deleted...).
15844 return;
15845 }
15846
15847 $current_blog_id = get_current_blog_id();
15848
15849 $this->switch_to_blog( $context_blog_id );
15850
15851 // Send re-activation event.
15852 $this->sync_install( array(
15853 'is_active' => true,
15854 ) );
15855
15856 $this->switch_to_blog( $current_blog_id );
15857 }
15858
15859 #endregion Multisite
15860
15861 /**
15862 * @author Leo Fajardo (@leorw)
15863 *
15864 * @param string $path
15865 * @param string $scheme
15866 * @param bool $network
15867 *
15868 * @return string
15869 */
15870 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
15871 return ( $this->_is_network_active && $network ) ?
15872 network_admin_url( $path, $scheme ) :
15873 admin_url( $path, $scheme );
15874 }
15875
15876 /**
15877 * Check if currently in a specified admin page.
15878 *
15879 * @author Vova Feldman (@svovaf)
15880 * @since 1.2.2.7
15881 *
15882 * @param string $page
15883 *
15884 * @return bool
15885 */
15886 function is_admin_page( $page ) {
15887 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
15888 }
15889
15890 /**
15891 * Check if currently in the product's main admin page.
15892 *
15893 * @author Vova Feldman (@svovaf)
15894 * @since 2.3.1
15895 *
15896 * @return bool
15897 */
15898 function is_main_admin_page() {
15899 return $this->is_admin_page( '' );
15900 }
15901
15902 /**
15903 * Get module's main admin setting page URL.
15904 *
15905 * @author Vova Feldman (@svovaf)
15906 * @since 1.2.2.7
15907 *
15908 * @return string
15909 */
15910 function main_menu_url() {
15911 return $this->_menu->main_menu_url();
15912 }
15913
15914 /**
15915 * Check if currently on the theme's setting page or
15916 * on any of the Freemius added pages (via tabs).
15917 *
15918 * @author Vova Feldman (@svovaf)
15919 * @since 1.2.2.7
15920 *
15921 * @return bool
15922 *
15923 * @deprecated Please use is_product_settings_page() instead;
15924 */
15925 function is_theme_settings_page() {
15926 return $this->is_product_settings_page();
15927 }
15928
15929 /**
15930 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
15931 *
15932 * @author Vova Feldman (@svovaf)
15933 * @since 1.2.2.7
15934 *
15935 * @return bool
15936 */
15937 function is_product_settings_page() {
15938 return fs_starts_with(
15939 fs_request_get( 'page', '', 'get' ),
15940 $this->_menu->get_slug()
15941 );
15942 }
15943
15944 /**
15945 * Plugin's account page + sync license URL.
15946 *
15947 * @author Vova Feldman (@svovaf)
15948 * @since 1.1.9.1
15949 *
15950 * @param bool|number $plugin_id
15951 * @param bool $add_action_nonce
15952 * @param array $params
15953 *
15954 * @return string
15955 */
15956 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
15957 if ( is_numeric( $plugin_id ) ) {
15958 $params['plugin_id'] = $plugin_id;
15959 }
15960
15961 return $this->get_account_url(
15962 $this->get_unique_affix() . '_sync_license',
15963 $params,
15964 $add_action_nonce
15965 );
15966 }
15967
15968 /**
15969 * Plugin's account URL.
15970 *
15971 * @author Vova Feldman (@svovaf)
15972 * @since 1.0.4
15973 *
15974 * @param bool|string $action
15975 * @param array $params
15976 *
15977 * @param bool $add_action_nonce
15978 *
15979 * @return string
15980 */
15981 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
15982 if ( is_string( $action ) ) {
15983 $params['fs_action'] = $action;
15984 }
15985
15986 self::require_pluggable_essentials();
15987
15988 return ( $add_action_nonce && is_string( $action ) ) ?
15989 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
15990 $this->_get_admin_page_url( 'account', $params );
15991 }
15992
15993 /**
15994 * @author Vova Feldman (@svovaf)
15995 * @since 1.2.0
15996 *
15997 * @param string $tab
15998 * @param bool $action
15999 * @param array $params
16000 * @param bool $add_action_nonce
16001 *
16002 * @return string
16003 *
16004 * @uses get_account_url()
16005 */
16006 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16007 $params['tab'] = $tab;
16008
16009 return $this->get_account_url( $action, $params, $add_action_nonce );
16010 }
16011
16012 /**
16013 * Plugin's account URL.
16014 *
16015 * @author Vova Feldman (@svovaf)
16016 * @since 1.0.4
16017 *
16018 * @param bool|string $topic
16019 * @param bool|string $message
16020 *
16021 * @return string
16022 */
16023 function contact_url( $topic = false, $message = false ) {
16024 $params = array();
16025 if ( is_string( $topic ) ) {
16026 $params['topic'] = $topic;
16027 }
16028 if ( is_string( $message ) ) {
16029 $params['message'] = $message;
16030 }
16031
16032 if ( $this->is_addon() ) {
16033 $params['addon_id'] = $this->get_id();
16034
16035 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16036 } else {
16037 return $this->_get_admin_page_url( 'contact', $params );
16038 }
16039 }
16040
16041 /**
16042 * Add-on direct info URL.
16043 *
16044 * @author Vova Feldman (@svovaf)
16045 * @since 1.1.0
16046 *
16047 * @param string $slug
16048 *
16049 * @return string
16050 */
16051 function addon_url( $slug ) {
16052 return $this->_get_admin_page_url( 'addons', array(
16053 'slug' => $slug
16054 ) );
16055 }
16056
16057 /**
16058 * Add-ons URL.
16059 *
16060 * @author Vova Feldman (@svovaf)
16061 * @since 2.4.5
16062 *
16063 * @return string
16064 */
16065 function get_addons_url() {
16066 return $this->_get_admin_page_url( 'addons' );
16067 }
16068
16069 /* Logger
16070 ------------------------------------------------------------------------------------------------------------------*/
16071 /**
16072 * @param string $id
16073 * @param bool $prefix_slug
16074 *
16075 * @return FS_Logger
16076 */
16077 function get_logger( $id = '', $prefix_slug = true ) {
16078 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16079 }
16080
16081 /**
16082 * Note: This method is used externally so don't delete it.
16083 *
16084 * @param $id
16085 * @param bool $load_options
16086 * @param bool $prefix_slug
16087 *
16088 * @return FS_Option_Manager
16089 */
16090 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16091 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16092 }
16093
16094 /* Security
16095 ------------------------------------------------------------------------------------------------------------------*/
16096 private static function _encrypt( $str ) {
16097 if ( is_null( $str ) ) {
16098 return null;
16099 }
16100
16101 /**
16102 * The encrypt/decrypt functions are used to protect
16103 * the user from messing up with some of the sensitive
16104 * data stored for the module as a JSON in the database.
16105 *
16106 * I used the same suggested hack by the theme review team.
16107 * For more details, look at the function `Base64UrlDecode()`
16108 * in `./sdk/FreemiusBase.php`.
16109 *
16110 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16111 *
16112 * @author Vova Feldman (@svovaf)
16113 * @since 1.2.2
16114 */
16115 $fn = 'base64' . '_encode';
16116
16117 return $fn( $str );
16118 }
16119
16120 static function _decrypt( $str ) {
16121 if ( is_null( $str ) ) {
16122 return null;
16123 }
16124
16125 /**
16126 * The encrypt/decrypt functions are used to protect
16127 * the user from messing up with some of the sensitive
16128 * data stored for the module as a JSON in the database.
16129 *
16130 * I used the same suggested hack by the theme review team.
16131 * For more details, look at the function `Base64UrlDecode()`
16132 * in `./sdk/FreemiusBase.php`.
16133 *
16134 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16135 *
16136 * @author Vova Feldman (@svovaf)
16137 * @since 1.2.2
16138 */
16139 $fn = 'base64' . '_decode';
16140
16141 return $fn( $str );
16142 }
16143
16144 /**
16145 * @author Vova Feldman (@svovaf)
16146 * @since 1.0.5
16147 *
16148 * @param FS_Entity $entity
16149 *
16150 * @return FS_Entity Return an encrypted clone entity.
16151 */
16152 private static function _encrypt_entity( FS_Entity $entity ) {
16153 $clone = clone $entity;
16154 $props = get_object_vars( $entity );
16155
16156 foreach ( $props as $key => $val ) {
16157 $clone->{$key} = self::_encrypt( $val );
16158 }
16159
16160 return $clone;
16161 }
16162
16163 /**
16164 * @author Vova Feldman (@svovaf)
16165 * @since 1.0.5
16166 *
16167 * @param FS_Entity $entity
16168 *
16169 * @return FS_Entity Return an decrypted clone entity.
16170 */
16171 private static function decrypt_entity( FS_Entity $entity ) {
16172 $clone = clone $entity;
16173 $props = get_object_vars( $entity );
16174
16175 foreach ( $props as $key => $val ) {
16176 $clone->{$key} = self::_decrypt( $val );
16177 }
16178
16179 return $clone;
16180 }
16181
16182 /**
16183 * Tries to activate account based on POST params.
16184 *
16185 * @author Vova Feldman (@svovaf)
16186 * @since 1.0.2
16187 *
16188 * @deprecated Not in use, outdated.
16189 */
16190 function _activate_account() {
16191 if ( $this->is_registered() ) {
16192 // Already activated.
16193 return;
16194 }
16195
16196 self::_clean_admin_content_section();
16197
16198 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
16199 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
16200
16201 // Verify matching plugin details.
16202 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
16203 return;
16204 }
16205
16206 $user = new FS_User();
16207 $user->id = fs_request_get( 'user_id' );
16208 $user->public_key = fs_request_get( 'user_public_key' );
16209 $user->secret_key = fs_request_get( 'user_secret_key' );
16210 $user->email = fs_request_get( 'user_email' );
16211 $user->first = fs_request_get( 'user_first' );
16212 $user->last = fs_request_get( 'user_last' );
16213 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
16214
16215 $site = new FS_Site();
16216 $site->id = fs_request_get( 'install_id' );
16217 $site->public_key = fs_request_get( 'install_public_key' );
16218 $site->secret_key = fs_request_get( 'install_secret_key' );
16219 $site->plan_id = fs_request_get( 'plan_id' );
16220
16221 $plans = array();
16222 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
16223 foreach ( $plans_data as $p ) {
16224 $plan = new FS_Plugin_Plan( $p );
16225 if ( $site->plan_id == $plan->id ) {
16226 $plan->title = fs_request_get( 'plan_title' );
16227 $plan->name = fs_request_get( 'plan_name' );
16228 }
16229
16230 $plans[] = $plan;
16231 }
16232
16233 $this->_set_account( $user, $site, $plans );
16234
16235 // Reload the page with the keys.
16236 fs_redirect( $this->_get_admin_page_url() );
16237 }
16238 }
16239
16240 /**
16241 * @author Vova Feldman (@svovaf)
16242 * @since 1.0.7
16243 *
16244 * @param string $email
16245 *
16246 * @return FS_User|false
16247 */
16248 static function _get_user_by_email( $email ) {
16249 self::$_static_logger->entrance();
16250
16251 $email = trim( strtolower( $email ) );
16252
16253 $users = self::get_all_users();
16254
16255 if ( is_array( $users ) ) {
16256 foreach ( $users as $user ) {
16257 if ( $email === trim( strtolower( $user->email ) ) ) {
16258 return $user;
16259 }
16260 }
16261 }
16262
16263 return false;
16264 }
16265
16266 #----------------------------------------------------------------------------------
16267 #region Account (Loading, Updates & Activation)
16268 #----------------------------------------------------------------------------------
16269
16270 /***
16271 * Load account information (user + site).
16272 *
16273 * @author Vova Feldman (@svovaf)
16274 * @since 1.0.1
16275 */
16276 private function _load_account() {
16277 $this->_logger->entrance();
16278
16279 $this->do_action( 'before_account_load' );
16280
16281 $users = self::get_all_users();
16282 $plans = self::get_all_plans( $this->_module_type );
16283
16284 if ( $this->_logger->is_on() && is_admin() ) {
16285 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16286 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16287 }
16288
16289 $site = fs_is_network_admin() ?
16290 $this->get_network_install() :
16291 $this->get_install_by_blog_id();
16292
16293 if ( fs_is_network_admin() &&
16294 $this->is_network_active() &&
16295 ! is_object( $site ) &&
16296 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16297 ) {
16298 $first_install = $this->find_first_install();
16299
16300 if ( is_null( $first_install ) ) {
16301 unset( $this->_storage->network_install_blog_id );
16302 } else {
16303 $site = $first_install['install'];
16304 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16305 }
16306 }
16307
16308 if ( is_object( $site ) &&
16309 is_numeric( $site->id ) &&
16310 is_numeric( $site->user_id ) &&
16311 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16312 ) {
16313 // Load site.
16314 $this->_site = $site;
16315
16316 // Load plans.
16317 $this->_plans = $plans[ $this->_slug ];
16318 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16319 $this->_sync_plans();
16320 } else {
16321 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16322 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16323 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16324 } else {
16325 unset( $this->_plans[ $i ] );
16326 }
16327 }
16328 }
16329 }
16330
16331 $user = null;
16332 if ( fs_is_network_admin() && $this->_is_network_active ) {
16333 $user = $this->get_network_user();
16334 }
16335
16336 if ( is_object( $user ) ) {
16337 $this->_user = clone $user;
16338 } else if ( $this->_site ) {
16339 $user = self::_get_user_by_id( $this->_site->user_id );
16340
16341 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16342 /**
16343 * Try to load the previous owner. This recovery is used for the following use-case:
16344 * 1. Opt-in
16345 * 2. Cloning site1 to site2
16346 * 3. Ownership switch in site1 (same applies for site2)
16347 * 4. Install data sync on site2
16348 * 5. Now site2's install is associated with the new owner which does not exists locally.
16349 */
16350 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16351 }
16352
16353 if ( ! is_object( $user ) ) {
16354 /**
16355 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16356 */
16357 $user = $this->sync_user_by_current_install();
16358 }
16359
16360 $this->_user = ( $user instanceof FS_User ) ?
16361 clone $user :
16362 null;
16363 }
16364
16365 if ( is_object( $this->_user ) ) {
16366 // Load licenses.
16367 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16368 }
16369
16370 if ( is_object( $this->_site ) ) {
16371 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16372
16373 if ( $this->_site->version != $this->get_plugin_version() ) {
16374 // If stored install version is different than current installed plugin version,
16375 // then update plugin version event.
16376 $this->update_plugin_version_event();
16377 }
16378 }
16379
16380 if ( true === $this->_storage->require_license_activation &&
16381 ! fs_request_get_bool( 'require_license', true )
16382 ) {
16383 $this->_storage->require_license_activation = false;
16384 }
16385
16386 if ( $this->is_theme() ) {
16387 $this->_register_account_hooks();
16388 }
16389 }
16390
16391 /**
16392 * Special user recovery mechanism.
16393 *
16394 * @author Vova Feldman (@svovaf)
16395 * @since 2.0.0
16396 *
16397 * @param number|null $site_user_id
16398 *
16399 * @return \FS_User|mixed
16400 */
16401 private function sync_user_by_current_install( $site_user_id = null ) {
16402 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16403 $site_user_id :
16404 $this->_site->user_id;
16405
16406 $api = $this->get_api_site_scope();
16407
16408 $uid = $this->get_anonymous_id();
16409 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16410
16411 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16412
16413 if ( $this->is_api_result_entity( $result ) ) {
16414 $user = new FS_User( $result );
16415 $this->_user = $user;
16416 $this->_store_user();
16417
16418 return $user;
16419 }
16420
16421 $error_code = FS_Api::get_error_code( $result );
16422
16423 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16424 /**
16425 * Those API errors will continue coming and are not recoverable with the
16426 * current site's data. Therefore, extend the API call's cached result to 7 days.
16427 */
16428 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16429 }
16430
16431 return $result;
16432 }
16433
16434 /**
16435 * @author Vova Feldman (@svovaf)
16436 * @since 1.0.1
16437 *
16438 * @param FS_User $user
16439 * @param FS_Site $site
16440 * @param bool|array $plans
16441 */
16442 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16443 $site->user_id = $user->id;
16444
16445 $this->_site = $site;
16446 $this->_user = $user;
16447 if ( false !== $plans ) {
16448 $this->_plans = $plans;
16449 }
16450
16451 $this->send_install_update();
16452
16453 $this->_store_account();
16454
16455 }
16456
16457 /**
16458 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16459 * Each version is trimmed after the 16th char.
16460 *
16461 * @author Vova Feldman (@svovaf)
16462 * @since 2.2.1
16463 *
16464 * @return array
16465 */
16466 private function get_versions() {
16467 $versions = array();
16468 $versions['platform_version'] = get_bloginfo( 'version' );
16469 $versions['sdk_version'] = $this->version;
16470 $versions['programming_language_version'] = phpversion();
16471
16472 foreach ( $versions as $k => $version ) {
16473 if ( is_string( $versions[ $k ] ) && ! empty( $versions[ $k ] ) ) {
16474 $versions[ $k ] = substr( $versions[ $k ], 0, 16 );
16475 }
16476 }
16477
16478 return $versions;
16479 }
16480
16481 /**
16482 * @author Leo Fajardo (@leorw)
16483 * @since 2.3.0
16484 *
16485 * @return bool
16486 */
16487 function has_beta_update() {
16488 return (
16489 ! empty( $this->_storage->beta_data ) &&
16490 ( true === $this->_storage->beta_data['is_beta'] ) &&
16491 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16492 );
16493 }
16494
16495 /**
16496 * @author Leo Fajardo (@leorw)
16497 * @since 2.3.0
16498 *
16499 * @return bool
16500 */
16501 function is_beta() {
16502 return (
16503 ! empty( $this->_storage->beta_data ) &&
16504 ( true === $this->_storage->beta_data['is_beta'] ) &&
16505 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16506 );
16507 }
16508
16509 /**
16510 * @author Vova Feldman (@svovaf)
16511 * @since 1.1.7.4
16512 *
16513 * @param array $override_with
16514 * @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.
16515 *
16516 * @return array
16517 */
16518 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
16519 $this->_logger->entrance();
16520
16521 $current_user = self::_get_current_wp_user();
16522
16523 $activation_action = $this->get_unique_affix() . '_activate_new';
16524 $return_url = $this->is_anonymous() ?
16525 // If skipped already, then return to the account page.
16526 $this->get_account_url( $activation_action, array(), false ) :
16527 // Return to the module's main page.
16528 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16529
16530 $versions = $this->get_versions();
16531
16532 $params = array_merge( $versions, array(
16533 'user_firstname' => $current_user->user_firstname,
16534 'user_lastname' => $current_user->user_lastname,
16535 'user_nickname' => $current_user->user_nicename,
16536 'user_email' => $current_user->user_email,
16537 'user_ip' => WP_FS__REMOTE_ADDR,
16538 'plugin_slug' => $this->_slug,
16539 'plugin_id' => $this->get_id(),
16540 'plugin_public_key' => $this->get_public_key(),
16541 'plugin_version' => $this->get_plugin_version(),
16542 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16543 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16544 'account',
16545 array( 'fs_action' => 'sync_user' )
16546 ), 'sync_user' ),
16547 'is_premium' => $this->is_premium(),
16548 'is_active' => true,
16549 'is_uninstalled' => false,
16550 ) );
16551
16552 if ( $this->is_addon() ) {
16553 $parent_fs = $this->get_parent_instance();
16554
16555 $params['parent_plugin_slug'] = $parent_fs->_slug;
16556 $params['parent_plugin_id'] = $parent_fs->get_id();
16557 }
16558
16559 if ( true === $network_level_or_blog_id ) {
16560 if ( ! isset( $override_with['sites'] ) ) {
16561 $params['sites'] = $this->get_sites_for_network_level_optin();
16562 }
16563 } else {
16564 $site = is_numeric( $network_level_or_blog_id ) ?
16565 array( 'blog_id' => $network_level_or_blog_id ) :
16566 null;
16567
16568 $site = $this->get_site_info( $site );
16569
16570 $params = array_merge( $params, array(
16571 'site_uid' => $site['uid'],
16572 'site_url' => $site['url'],
16573 'site_name' => $site['title'],
16574 'language' => $site['language'],
16575 'charset' => $site['charset'],
16576 ) );
16577 }
16578
16579 if ( $this->is_pending_activation() &&
16580 ! empty( $this->_storage->pending_license_key )
16581 ) {
16582 $params['license_key'] = $this->_storage->pending_license_key;
16583 }
16584
16585 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
16586 // Even though rand() is known for its security issues,
16587 // the timestamp adds another layer of protection.
16588 // It would be very hard for an attacker to get the secret key form here.
16589 // Plus, this should never run in production since the secret should never
16590 // be included in the production version.
16591 $params['ts'] = WP_FS__SCRIPT_START_TIME;
16592 $params['salt'] = md5( uniqid( rand() ) );
16593 $params['secure'] = md5(
16594 $params['ts'] .
16595 $params['salt'] .
16596 $this->get_secret_key()
16597 );
16598 }
16599
16600 return array_merge( $params, $override_with );
16601 }
16602
16603 /**
16604 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
16605 * 2. If there was an API error, return the API result.
16606 *
16607 * @author Vova Feldman (@svovaf)
16608 * @since 1.1.7.4
16609 *
16610 * @param string|bool $email
16611 * @param string|bool $first
16612 * @param string|bool $last
16613 * @param string|bool $license_key
16614 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
16615 * In this case, the user and site info will be sent to the server but no
16616 * data will be saved to the WP installation's database.
16617 * @param number|bool $trial_plan_id
16618 * @param bool $is_disconnected Whether or not to opt in without tracking.
16619 * @param null|bool $is_marketing_allowed
16620 * @param array $sites If network-level opt-in, an array of containing details of sites.
16621 *
16622 * @return string|object
16623 * @use WP_Error
16624 */
16625 function opt_in(
16626 $email = false,
16627 $first = false,
16628 $last = false,
16629 $license_key = false,
16630 $is_uninstall = false,
16631 $trial_plan_id = false,
16632 $is_disconnected = false,
16633 $is_marketing_allowed = null,
16634 $sites = array()
16635 ) {
16636 $this->_logger->entrance();
16637
16638 if ( false === $email ) {
16639 $current_user = self::_get_current_wp_user();
16640 $email = $current_user->user_email;
16641 }
16642
16643 /**
16644 * @since 1.2.1 If activating with license key, ignore the context-user
16645 * since the user will be automatically loaded from the license.
16646 */
16647 if ( empty( $license_key ) ) {
16648 // Clean up pending license if opt-ing in again.
16649 $this->_storage->remove( 'pending_license_key' );
16650
16651 if ( ! $is_uninstall ) {
16652 $fs_user = Freemius::_get_user_by_email( $email );
16653 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
16654 return $this->install_with_user(
16655 $fs_user,
16656 false,
16657 $trial_plan_id,
16658 true,
16659 true,
16660 $sites
16661 );
16662 }
16663 }
16664 }
16665
16666 $user_info = array();
16667 if ( ! empty( $email ) ) {
16668 $user_info['user_email'] = $email;
16669 }
16670 if ( ! empty( $first ) ) {
16671 $user_info['user_firstname'] = $first;
16672 }
16673 if ( ! empty( $last ) ) {
16674 $user_info['user_lastname'] = $last;
16675 }
16676
16677 if ( ! empty( $sites ) ) {
16678 $is_network = true;
16679
16680 $user_info['sites'] = $sites;
16681 } else {
16682 $is_network = false;
16683 }
16684
16685 $params = $this->get_opt_in_params( $user_info, $is_network );
16686
16687 $filtered_license_key = false;
16688 if ( is_string( $license_key ) ) {
16689 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
16690 $params['license_key'] = $filtered_license_key;
16691 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
16692 $params['trial_plan_id'] = $trial_plan_id;
16693 }
16694
16695 if ( $is_uninstall ) {
16696 $params['uninstall_params'] = array(
16697 'reason_id' => $this->_storage->uninstall_reason->id,
16698 'reason_info' => $this->_storage->uninstall_reason->info
16699 );
16700 }
16701
16702 if ( isset( $params['license_key'] ) ) {
16703 $fs_user = Freemius::_get_user_by_email( $email );
16704
16705 if ( is_object( $fs_user ) ) {
16706 /**
16707 * If opting in with a context license and the context WP Admin user already opted in
16708 * before from the current site, add the user context security params to avoid the
16709 * unnecessary email activation when the context license is owned by the same context user.
16710 *
16711 * @author Leo Fajardo (@leorw)
16712 * @since 1.2.3
16713 */
16714 $params = array_merge( $params, FS_Security::instance()->get_context_params(
16715 $fs_user,
16716 false,
16717 'install_with_existing_user'
16718 ) );
16719 }
16720 }
16721
16722 if ( is_bool( $is_marketing_allowed ) ) {
16723 $params['is_marketing_allowed'] = $is_marketing_allowed;
16724 }
16725
16726 $params['is_disconnected'] = $is_disconnected;
16727 $params['format'] = 'json';
16728
16729 $request = array(
16730 'method' => 'POST',
16731 'body' => $params,
16732 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
16733 );
16734
16735 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
16736 $response = self::safe_remote_post( $url, $request );
16737
16738 if ( is_wp_error( $response ) ) {
16739 /**
16740 * @var WP_Error $response
16741 */
16742 $result = new stdClass();
16743
16744 $error_code = $response->get_error_code();
16745 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
16746
16747 $result->error = (object) array(
16748 'type' => $error_type,
16749 'message' => $response->get_error_message(),
16750 'code' => $error_code,
16751 'http' => 402
16752 );
16753
16754 $this->maybe_modify_api_curl_error_message( $result );
16755
16756 return $result;
16757 }
16758
16759 // Module is being uninstalled, don't handle the returned data.
16760 if ( $is_uninstall ) {
16761 return true;
16762 }
16763
16764 /**
16765 * 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.
16766 *
16767 * @author Vova Feldman (@svovaf)
16768 * @since 1.2.3
16769 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
16770 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
16771 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
16772 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
16773 */
16774 $decoded = is_string( $response['body'] ) ?
16775 json_decode( $response['body'] ) :
16776 null;
16777
16778 if ( empty( $decoded ) ) {
16779 return false;
16780 }
16781
16782 if ( ! $this->is_api_result_object( $decoded ) ) {
16783 if ( ! empty( $params['license_key'] ) ) {
16784 // Pass the fully entered license key to the failure handler.
16785 $params['license_key'] = $license_key;
16786 }
16787
16788 return $is_uninstall ?
16789 $decoded :
16790 $this->apply_filters( 'after_install_failure', $decoded, $params );
16791 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
16792 if ( $is_network ) {
16793 $site_ids = array();
16794 foreach ( $sites as $site ) {
16795 $site_ids[] = $site['blog_id'];
16796 }
16797
16798 /**
16799 * Store the sites so that they can be installed once the user has clicked on the activation link
16800 * in the email.
16801 *
16802 * @author Leo Fajardo (@leorw)
16803 */
16804 $this->_storage->pending_sites_info = array(
16805 'blog_ids' => $site_ids,
16806 'license_key' => $license_key,
16807 'trial_plan_id' => $trial_plan_id
16808 );
16809 }
16810
16811 // Pending activation, add message.
16812 return $this->set_pending_confirmation(
16813 ( isset( $decoded->email ) ?
16814 $decoded->email :
16815 true ),
16816 false,
16817 $filtered_license_key,
16818 ! empty( $params['trial_plan_id'] )
16819 );
16820 } else if ( isset( $decoded->install_secret_key ) ) {
16821 return $this->install_with_new_user(
16822 $decoded->user_id,
16823 $decoded->user_public_key,
16824 $decoded->user_secret_key,
16825 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
16826 $decoded->is_marketing_allowed :
16827 null ),
16828 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
16829 $decoded->is_extensions_tracking_allowed :
16830 null ),
16831 $decoded->install_id,
16832 $decoded->install_public_key,
16833 $decoded->install_secret_key,
16834 false
16835 );
16836 } else if ( is_array( $decoded->installs ) ) {
16837 return $this->install_many_with_new_user(
16838 $decoded->user_id,
16839 $decoded->user_public_key,
16840 $decoded->user_secret_key,
16841 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
16842 $decoded->is_marketing_allowed :
16843 null ),
16844 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
16845 $decoded->is_extensions_tracking_allowed :
16846 null ),
16847 $decoded->installs,
16848 false
16849 );
16850 }
16851
16852 return $decoded;
16853 }
16854
16855 /**
16856 * Set user and site identities.
16857 *
16858 * @author Vova Feldman (@svovaf)
16859 * @since 1.0.9
16860 *
16861 * @param FS_User $user
16862 * @param FS_Site $site
16863 * @param bool $redirect
16864 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
16865 * redirect (or return a URL) to the account page with a special parameter to
16866 * trigger the auto installation processes.
16867 *
16868 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16869 */
16870 function setup_account(
16871 FS_User $user,
16872 FS_Site $site,
16873 $redirect = true,
16874 $auto_install = false
16875 ) {
16876 return $this->setup_network_account(
16877 $user,
16878 array( $site ),
16879 $redirect,
16880 $auto_install,
16881 false
16882 );
16883 }
16884
16885 /**
16886 * Set user and site identities.
16887 *
16888 * @author Vova Feldman (@svovaf)
16889 * @since 2.0.0
16890 *
16891 * @param FS_User $user
16892 * @param FS_Site[] $installs
16893 * @param bool $redirect
16894 * @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.
16895 * @param bool $is_network_level_opt_in
16896 *
16897 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16898 */
16899 function setup_network_account(
16900 FS_User $user,
16901 array $installs,
16902 $redirect = true,
16903 $auto_install = false,
16904 $is_network_level_opt_in = true
16905 ) {
16906 $first_install = $installs[0];
16907
16908 $this->_user = $user;
16909 $this->_site = $first_install;
16910
16911 $this->_sync_plans();
16912
16913 if ( $this->_storage->handle_gdpr_admin_notice &&
16914 $this->should_handle_gdpr_admin_notice() &&
16915 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
16916 ) {
16917 /**
16918 * Clear user lock after an opt-in.
16919 */
16920 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
16921 FS_User_Lock::instance()->unlock();
16922 }
16923
16924 if ( 1 < count( $installs ) ) {
16925 // Only network level opt-in can have more than one install.
16926 $is_network_level_opt_in = true;
16927 }
16928 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
16929 // If Freemius was OFF before, turn it on.
16930 $this->turn_on();
16931
16932 $this->handle_account_connection(
16933 $installs,
16934 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
16935 );
16936
16937 if ( is_numeric( $first_install->license_id ) ) {
16938 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
16939 }
16940
16941 $this->_admin_notices->remove_sticky( 'connect_account' );
16942
16943 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
16944 // Remove pending activation sticky notice (if still exist).
16945 $this->_admin_notices->remove_sticky( 'activation_pending' );
16946
16947 // Remove plugin from pending activation mode.
16948 unset( $this->_storage->is_pending_activation );
16949
16950 if ( ! $this->is_paying_or_trial() ) {
16951 $this->_admin_notices->add_sticky(
16952 sprintf( $this->get_text_inline( '%s activation was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
16953 'activation_complete'
16954 );
16955 }
16956 }
16957
16958 if ( $this->is_paying_or_trial() ) {
16959 if ( ! $this->is_premium() ||
16960 ! $this->has_premium_version() ||
16961 ! $this->has_settings_menu()
16962 ) {
16963 if ( $this->is_paying() ) {
16964 $this->_admin_notices->add_sticky(
16965 sprintf(
16966 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
16967 $this->get_plan_title()
16968 ) . $this->get_complete_upgrade_instructions(),
16969 'plan_upgraded',
16970 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16971 );
16972 } else {
16973 $trial_plan = $this->get_trial_plan();
16974
16975 $this->_admin_notices->add_sticky(
16976 sprintf(
16977 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
16978 '<i>' . $this->get_plugin_name() . '</i>'
16979 ) . $this->get_complete_upgrade_instructions( $trial_plan->title ),
16980 'trial_started',
16981 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16982 );
16983 }
16984 }
16985
16986 $this->_admin_notices->remove_sticky( array(
16987 'trial_promotion',
16988 ) );
16989 }
16990
16991 $plugin_id = fs_request_get( 'plugin_id', false );
16992
16993 // Store activation time ONLY for plugins & themes (not add-ons).
16994 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
16995 if ( empty( $this->_storage->activation_timestamp ) ) {
16996 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
16997 }
16998 }
16999
17000 $next_page = '';
17001
17002 $extra = array();
17003 if ( $auto_install ) {
17004 $extra['auto_install'] = 'true';
17005 }
17006
17007 if ( is_numeric( $plugin_id ) ) {
17008 /**
17009 * @author Leo Fajardo (@leorw)
17010 * @since 1.2.1.6
17011 *
17012 * Also sync the license after an anonymous user subscribes.
17013 */
17014 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17015 // Add-on was installed - sync license right after install.
17016 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17017 }
17018 } else {
17019 /**
17020 * @author Vova Feldman (@svovaf)
17021 * @since 1.1.9 If site installed with a valid license, sync license.
17022 */
17023 if ( $this->is_paying() ) {
17024 $this->_sync_plugin_license(
17025 true,
17026 // Installs data is already synced in the beginning of this method directly or via _set_account().
17027 false
17028 );
17029 }
17030
17031 // Reload the page with the keys.
17032 $next_page = $this->is_anonymous() ?
17033 // If user previously skipped, redirect to account page.
17034 $this->get_account_url( false, $extra ) :
17035 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17036 }
17037
17038 if ( ! empty( $next_page ) && $redirect ) {
17039 fs_redirect( $next_page );
17040 }
17041
17042 return $next_page;
17043 }
17044
17045 /**
17046 * Install plugin with new user information after approval.
17047 *
17048 * @author Vova Feldman (@svovaf)
17049 * @since 1.0.7
17050 */
17051 function _install_with_new_user() {
17052 $this->_logger->entrance();
17053
17054 if ( $this->is_registered() ) {
17055 return;
17056 }
17057
17058 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17059 // @todo This logic should be improved because it's executed on every load of a theme.
17060 $this->is_theme()
17061 ) {
17062 // check_admin_referer( $this->_slug . '_activate_new' );
17063
17064 if ( fs_request_has( 'user_secret_key' ) ) {
17065 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17066 $pending_sites_info = $this->_storage->pending_sites_info;
17067
17068 $this->install_many_pending_with_user(
17069 fs_request_get( 'user_id' ),
17070 fs_request_get( 'user_public_key' ),
17071 fs_request_get( 'user_secret_key' ),
17072 fs_request_get_bool( 'is_marketing_allowed', null ),
17073 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17074 $pending_sites_info['blog_ids'],
17075 $pending_sites_info['license_key'],
17076 $pending_sites_info['trial_plan_id']
17077 );
17078 } else {
17079 $this->install_with_new_user(
17080 fs_request_get( 'user_id' ),
17081 fs_request_get( 'user_public_key' ),
17082 fs_request_get( 'user_secret_key' ),
17083 fs_request_get_bool( 'is_marketing_allowed', null ),
17084 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17085 fs_request_get( 'install_id' ),
17086 fs_request_get( 'install_public_key' ),
17087 fs_request_get( 'install_secret_key' ),
17088 true,
17089 fs_request_get_bool( 'auto_install' )
17090 );
17091 }
17092 } else if ( fs_request_has( 'pending_activation' ) ) {
17093 $this->set_pending_confirmation( fs_request_get( 'user_email' ), true );
17094 }
17095 }
17096 }
17097
17098 /**
17099 * @author Vova Feldman (@svovaf)
17100 * @since 2.0.0
17101 *
17102 * @param number $id
17103 * @param string $public_key
17104 * @param string $secret_key
17105 *
17106 * @return \FS_User
17107 */
17108 private function setup_user( $id, $public_key, $secret_key ) {
17109 $user = self::_get_user_by_id( $id );
17110
17111 if ( is_object( $user ) ) {
17112 $this->_user = $user;
17113 } else {
17114 $user = new FS_User();
17115 $user->id = $id;
17116 $user->public_key = $public_key;
17117 $user->secret_key = $secret_key;
17118
17119 $this->_user = $user;
17120 $user_result = $this->get_api_user_scope()->get();
17121 $user = new FS_User( $user_result );
17122
17123 $this->_user = $user;
17124 $this->_store_user();
17125 }
17126
17127 return $user;
17128 }
17129
17130 /**
17131 * Install plugin with new user.
17132 *
17133 * @author Vova Feldman (@svovaf)
17134 * @since 1.1.7.4
17135 *
17136 * @param number $user_id
17137 * @param string $user_public_key
17138 * @param string $user_secret_key
17139 * @param bool|null $is_marketing_allowed
17140 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17141 * @param number $install_id
17142 * @param string $install_public_key
17143 * @param string $install_secret_key
17144 * @param bool $redirect
17145 * @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.
17146 *
17147 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17148 */
17149 private function install_with_new_user(
17150 $user_id,
17151 $user_public_key,
17152 $user_secret_key,
17153 $is_marketing_allowed,
17154 $is_extensions_tracking_allowed,
17155 $install_id,
17156 $install_public_key,
17157 $install_secret_key,
17158 $redirect = true,
17159 $auto_install = false
17160 ) {
17161 /**
17162 * This method is also executed after opting in with a license key since the
17163 * license can be potentially associated with a different owner.
17164 *
17165 * @since 2.0.0
17166 */
17167 $user = self::_get_user_by_id( $user_id );
17168
17169 if ( ! is_object( $user ) ) {
17170 $user = new FS_User();
17171 $user->id = $user_id;
17172 $user->public_key = $user_public_key;
17173 $user->secret_key = $user_secret_key;
17174
17175 $this->_user = $user;
17176 $user_result = $this->get_api_user_scope()->get();
17177 $user = new FS_User( $user_result );
17178 }
17179
17180 $this->_user = $user;
17181
17182 $site = new FS_Site();
17183 $site->id = $install_id;
17184 $site->public_key = $install_public_key;
17185 $site->secret_key = $install_secret_key;
17186
17187 $this->_site = $site;
17188 $site_result = $this->get_api_site_scope()->get();
17189 $site = new FS_Site( $site_result );
17190 $this->_site = $site;
17191
17192 if ( ! is_null( $is_marketing_allowed ) ) {
17193 $this->disable_opt_in_notice_and_lock_user();
17194 }
17195
17196 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17197
17198 return $this->setup_account(
17199 $this->_user,
17200 $this->_site,
17201 $redirect,
17202 $auto_install
17203 );
17204 }
17205
17206 /**
17207 * Install plugin with user.
17208 *
17209 * @author Leo Fajardo (@leorw)
17210 * @since 2.0.0
17211 *
17212 * @param number $user_id
17213 * @param string $user_public_key
17214 * @param string $user_secret_key
17215 * @param bool|null $is_marketing_allowed
17216 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17217 * @param array $site_ids
17218 * @param bool $license_key
17219 * @param bool $trial_plan_id
17220 * @param bool $redirect
17221 *
17222 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17223 */
17224 private function install_many_pending_with_user(
17225 $user_id,
17226 $user_public_key,
17227 $user_secret_key,
17228 $is_marketing_allowed,
17229 $is_extensions_tracking_allowed,
17230 $site_ids,
17231 $license_key = false,
17232 $trial_plan_id = false,
17233 $redirect = true
17234 ) {
17235 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17236
17237 if ( ! is_null( $is_marketing_allowed ) ) {
17238 $this->disable_opt_in_notice_and_lock_user();
17239 }
17240
17241 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17242
17243 $sites = array();
17244 foreach ( $site_ids as $site_id ) {
17245 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17246 }
17247
17248 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17249 }
17250
17251 /**
17252 * Multi-site install with a new user.
17253 *
17254 * @author Vova Feldman (@svovaf)
17255 * @since 2.0.0
17256 *
17257 * @param number $user_id
17258 * @param string $user_public_key
17259 * @param string $user_secret_key
17260 * @param bool|null $is_marketing_allowed
17261 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17262 * @param object[] $installs
17263 * @param bool $redirect
17264 * @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.
17265 *
17266 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17267 */
17268 private function install_many_with_new_user(
17269 $user_id,
17270 $user_public_key,
17271 $user_secret_key,
17272 $is_marketing_allowed,
17273 $is_extensions_tracking_allowed,
17274 array $installs,
17275 $redirect = true,
17276 $auto_install = false
17277 ) {
17278 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17279
17280 if ( ! is_null( $is_marketing_allowed ) ) {
17281 $this->disable_opt_in_notice_and_lock_user();
17282 }
17283
17284 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17285
17286 $install_ids = array();
17287
17288 foreach ( $installs as $install ) {
17289 $install_ids[] = $install->id;
17290 }
17291
17292 $left = count( $install_ids );
17293 $offset = 0;
17294
17295 $installs = array();
17296 while ( $left > 0 ) {
17297 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, 25 ) ) );
17298
17299 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17300 // @todo Handle API error.
17301 }
17302
17303 $installs = array_merge( $installs, $result->installs );
17304
17305 $left -= 25;
17306 }
17307
17308 foreach ( $installs as &$install ) {
17309 $install = new FS_Site( $install );
17310 }
17311
17312 return $this->setup_network_account(
17313 $this->_user,
17314 $installs,
17315 $redirect,
17316 $auto_install
17317 );
17318 }
17319
17320 /**
17321 * @author Vova Feldman (@svovaf)
17322 * @since 1.1.7.4
17323 *
17324 * @param string|bool $email
17325 * @param bool $redirect
17326 * @param string|bool $license_key Since 1.2.1.5
17327 * @param bool $is_pending_trial Since 1.2.1.5
17328 *
17329 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17330 */
17331 private function set_pending_confirmation(
17332 $email = false,
17333 $redirect = true,
17334 $license_key = false,
17335 $is_pending_trial = false
17336 ) {
17337 if ( $this->_ignore_pending_mode ) {
17338 /**
17339 * If explicitly asked to ignore pending mode, set to anonymous mode
17340 * if require confirmation before finalizing the opt-in.
17341 *
17342 * @author Vova Feldman
17343 * @since 1.2.1.6
17344 */
17345 $this->skip_connection( null, fs_is_network_admin() );
17346 } else {
17347 // Install must be activated via email since
17348 // user with the same email already exist.
17349 $this->_storage->is_pending_activation = true;
17350 $this->_add_pending_activation_notice( $email, $is_pending_trial );
17351 }
17352
17353 if ( ! empty( $license_key ) ) {
17354 $this->_storage->pending_license_key = $license_key;
17355 }
17356
17357 // Remove the opt-in sticky notice.
17358 $this->_admin_notices->remove_sticky( array(
17359 'connect_account',
17360 'trial_promotion',
17361 ) );
17362
17363 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17364
17365 // Reload the page with with pending activation message.
17366 if ( $redirect ) {
17367 fs_redirect( $next_page );
17368 }
17369
17370 return $next_page;
17371 }
17372
17373 /**
17374 * Install plugin with current logged WP user info.
17375 *
17376 * @author Vova Feldman (@svovaf)
17377 * @since 1.0.7
17378 */
17379 function _install_with_current_user() {
17380 $this->_logger->entrance();
17381
17382 if ( $this->is_registered() ) {
17383 return;
17384 }
17385
17386 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17387 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
17388
17389 /**
17390 * @author Vova Feldman (@svovaf)
17391 * @since 1.1.9 Add license key if given.
17392 */
17393 $license_key = fs_request_get( 'license_secret_key' );
17394
17395 $this->update_extensions_tracking_flag( fs_request_get_bool( 'is_extensions_tracking_allowed', null ) );
17396
17397 $this->install_with_current_user( $license_key );
17398 }
17399 }
17400
17401
17402 /**
17403 * @author Vova Feldman (@svovaf)
17404 * @since 1.1.7.4
17405 *
17406 * @param string|bool $license_key
17407 * @param number|bool $trial_plan_id
17408 * @param array $sites Since 2.0.0
17409 * @param bool $redirect
17410 *
17411 * @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.
17412 */
17413 private function install_with_current_user(
17414 $license_key = false,
17415 $trial_plan_id = false,
17416 $sites = array(),
17417 $redirect = true
17418 ) {
17419 // Get current logged WP user.
17420 $current_user = self::_get_current_wp_user();
17421
17422 // Find the relevant FS user by the email.
17423 $user = self::_get_user_by_email( $current_user->user_email );
17424
17425 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17426 }
17427
17428 /**
17429 * @author Vova Feldman (@svovaf)
17430 * @since 2.0.0
17431 *
17432 * @param \FS_User $user
17433 * @param string|bool $license_key
17434 * @param number|bool $trial_plan_id
17435 * @param bool $redirect
17436 * @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.
17437 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
17438 *
17439 * @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.
17440 */
17441 function install_with_user(
17442 FS_User $user,
17443 $license_key = false,
17444 $trial_plan_id = false,
17445 $redirect = true,
17446 $setup_account = true,
17447 $sites = array()
17448 ) {
17449 // We have to set the user before getting user scope API handler.
17450 $this->_user = $user;
17451
17452 // Install the plugin.
17453 $result = $this->create_installs_with_user(
17454 $user,
17455 $license_key,
17456 $trial_plan_id,
17457 $sites,
17458 $redirect
17459 );
17460
17461 if ( ! $this->is_api_result_entity( $result ) &&
17462 ! $this->is_api_result_object( $result, 'installs' )
17463 ) {
17464 // @todo Handler potential API error of the $result
17465 }
17466
17467 if ( empty( $sites ) ) {
17468 $site = new FS_Site( $result );
17469 $this->_site = $site;
17470
17471 if ( ! $setup_account ) {
17472 $this->_store_site();
17473
17474 $this->sync_plan_if_not_exist( $site->plan_id );
17475
17476 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
17477 $this->sync_license_if_not_exist( $site->license_id, $license_key );
17478 }
17479
17480 $this->_admin_notices->remove_sticky( 'connect_account', false );
17481
17482 return $site;
17483 }
17484
17485 return $this->setup_account( $this->_user, $this->_site, $redirect );
17486 } else {
17487 $installs = array();
17488 foreach ( $result->installs as $install ) {
17489 $installs[] = new FS_Site( $install );
17490 }
17491
17492 return $this->setup_network_account(
17493 $user,
17494 $installs,
17495 $redirect
17496 );
17497 }
17498 }
17499
17500 /**
17501 * Initiate an API request to create a collection of installs.
17502 *
17503 * @author Vova Feldman (@svovaf)
17504 * @since 2.0.0
17505 *
17506 * @param \FS_User $user
17507 * @param bool $license_key
17508 * @param bool $trial_plan_id
17509 * @param array $sites
17510 * @param bool $redirect
17511 * @param bool $silent
17512 *
17513 * @return object|mixed
17514 */
17515 private function create_installs_with_user(
17516 FS_User $user,
17517 $license_key = false,
17518 $trial_plan_id = false,
17519 $sites = array(),
17520 $redirect = false,
17521 $silent = false
17522 ) {
17523 $extra_install_params = array(
17524 'uid' => $this->get_anonymous_id(),
17525 'is_disconnected' => false,
17526 );
17527
17528 if ( ! empty( $license_key ) ) {
17529 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
17530
17531 if ( $silent ) {
17532 $extra_install_params['ignore_license_owner'] = true;
17533 }
17534 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17535 $extra_install_params['trial_plan_id'] = $trial_plan_id;
17536 }
17537
17538 if ( ! empty( $sites ) ) {
17539 $extra_install_params['sites'] = $sites;
17540 }
17541
17542 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
17543
17544 // Install the plugin.
17545 $result = $this->get_api_user_scope_by_user( $user )->call(
17546 "/plugins/{$this->get_id()}/installs.json",
17547 'post',
17548 $args
17549 );
17550
17551 if ( ! $this->is_api_result_entity( $result ) &&
17552 ! $this->is_api_result_object( $result, 'installs' )
17553 ) {
17554 if ( ! empty( $args['license_key'] ) ) {
17555 // Pass the fully entered license key to the failure handler.
17556 $args['license_key'] = $license_key;
17557 }
17558
17559 $result = $this->apply_filters( 'after_install_failure', $result, $args );
17560
17561 if ( ! $silent ) {
17562 $this->_admin_notices->add(
17563 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17564 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
17565 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17566 'error'
17567 );
17568 }
17569
17570 if ( $redirect ) {
17571 /**
17572 * We set the user before getting the user scope API handler, so the user became temporarily
17573 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
17574 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
17575 * activation page based on the return value of `is_registered()`. In addition, in case the
17576 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
17577 * misleading plugin activation errors will be shown on the `Plugins` page.
17578 *
17579 * @author Leo Fajardo (@leorw)
17580 */
17581 $this->_user = null;
17582
17583 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
17584 }
17585 }
17586
17587 return $result;
17588 }
17589
17590 /**
17591 * Tries to activate add-on account based on parent plugin info.
17592 *
17593 * @author Vova Feldman (@svovaf)
17594 * @since 1.0.6
17595 *
17596 * @param Freemius $parent_fs
17597 * @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.
17598 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
17599 */
17600 private function _activate_addon_account(
17601 Freemius $parent_fs,
17602 $network_level_or_blog_id = null,
17603 FS_Plugin_License $bundle_license = null
17604 ) {
17605 if ( $this->is_registered() ) {
17606 // Already activated.
17607 return;
17608 }
17609
17610 /**
17611 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
17612 * already returns the data for the current blog.
17613 *
17614 * @author Leo Fajardo (@leorw)
17615 * @since 2.3.0
17616 */
17617 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
17618 array() :
17619 array( 'uid' => $this->get_anonymous_id() );
17620
17621 $params = $this->get_install_data_for_api(
17622 $uid_param_to_override,
17623 false,
17624 false,
17625 /**
17626 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
17627 * already includes the data for it.
17628 *
17629 * @author Leo Fajardo (@leorw)
17630 * @since 2.3.0
17631 */
17632 ( true !== $network_level_or_blog_id )
17633 );
17634
17635 if ( true === $network_level_or_blog_id ) {
17636 $params['sites'] = $this->get_sites_for_network_level_optin();
17637
17638 if ( empty( $params['sites'] ) ) {
17639 return;
17640 }
17641 }
17642
17643 if ( is_object( $bundle_license ) ) {
17644 $params['license_key'] = $bundle_license->secret_key;
17645 }
17646
17647 // Activate add-on with parent plugin credentials.
17648 $result = $parent_fs->get_api_site_scope()->call(
17649 "/addons/{$this->_plugin->id}/installs.json",
17650 'post',
17651 $params
17652 );
17653
17654 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17655 if ( is_object( $bundle_license ) ) {
17656 /**
17657 * 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.
17658 *
17659 * @author Leo Fajardo (@leorw)
17660 * @since 2.4.0
17661 */
17662 } else {
17663 $error_message = FS_Api::is_api_error_object( $result ) ?
17664 $result->error->message :
17665 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
17666
17667 $this->_admin_notices->add(
17668 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17669 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
17670 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17671 'error'
17672 );
17673 }
17674
17675 return;
17676 }
17677
17678 $addon_installs = $result->installs;
17679 foreach ( $addon_installs as $key => $addon_install ) {
17680 $addon_installs[ $key ] = new FS_Site( $addon_install );
17681 }
17682
17683 $first_install = $addon_installs[0];
17684
17685 // Get user information based on parent's plugin.
17686 $user = $parent_fs->get_user();
17687
17688 // First of all, set site and user info - otherwise we won't
17689 // be able to invoke API calls.
17690 $this->_site = $first_install;
17691 $this->_user = $user;
17692
17693 // Sync add-on plans.
17694 $this->_sync_plans();
17695
17696 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
17697
17698 // Get site's current plan.
17699 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
17700
17701 // Sync licenses.
17702 $this->_sync_licenses();
17703
17704 if ( ! fs_is_network_admin() ) {
17705 // Try to activate premium license.
17706 $this->_activate_license( true, $bundle_license );
17707
17708 if ( is_object( $bundle_license ) ) {
17709 $this->maybe_activate_bundle_license( $bundle_license );
17710 }
17711 } else {
17712 if ( is_object( $bundle_license ) ) {
17713 $premium_license = $bundle_license;
17714 } else {
17715 $license_id = fs_request_get( 'license_id' );
17716
17717 if ( is_object( $this->_site ) &&
17718 FS_Plugin_License::is_valid_id( $license_id ) &&
17719 $license_id == $this->_site->license_id
17720 ) {
17721 // License is already activated.
17722 return;
17723 }
17724
17725 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
17726 $this->_get_license_by_id( $license_id ) :
17727 $this->_get_available_premium_license();
17728 }
17729
17730 if ( is_object( $premium_license ) ) {
17731 $this->maybe_network_activate_addon_license( $premium_license );
17732 }
17733 }
17734 }
17735
17736 /**
17737 * @author Leo Fajardo (@leorw)
17738 * @since 2.3.0
17739 *
17740 * @param FS_Site[] $installs
17741 * @param bool $is_site_level
17742 */
17743 private function handle_account_connection( $installs, $is_site_level ) {
17744 $first_install = $installs[0];
17745
17746 if ( $is_site_level ) {
17747 $this->_set_account( $this->_user, $first_install );
17748
17749 $this->do_action( 'after_account_connection', $this->_user, $first_install );
17750 } else {
17751 $this->_store_user();
17752
17753 // Map site addresses to their blog IDs.
17754 $address_to_blog_map = $this->get_address_to_blog_map();
17755
17756 $first_blog_id = null;
17757 $blog_2_install_map = array();
17758 foreach ( $installs as $install ) {
17759 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
17760 $blog_id = $address_to_blog_map[ $address ];
17761
17762 $this->_store_site( true, $blog_id, $install );
17763
17764 if ( is_null( $first_blog_id ) ) {
17765 $first_blog_id = $blog_id;
17766 }
17767
17768 $blog_2_install_map[ $blog_id ] = $install;
17769 }
17770
17771 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
17772 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
17773 ) {
17774 // Store network user.
17775 $this->_storage->network_user_id = $this->_user->id;
17776 }
17777
17778 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
17779 $this->_storage->network_install_blog_id = $first_blog_id;
17780 }
17781
17782 if ( count( $installs ) === count( $address_to_blog_map ) ) {
17783 // Super admin opted in for all sites in the network.
17784 $this->_storage->is_network_connected = true;
17785 }
17786
17787 $this->_store_licenses( false );
17788
17789 self::$_accounts->store();
17790
17791 // Don't sync the installs data on network upgrade
17792 if ( ! $this->network_upgrade_mode_completed() ) {
17793 $this->send_installs_update();
17794 }
17795
17796 // Switch install context back to the first install.
17797 $this->_site = $first_install;
17798
17799 $current_blog = get_current_blog_id();
17800
17801 foreach ( $blog_2_install_map as $blog_id => $install ) {
17802 $this->switch_to_blog( $blog_id );
17803
17804 $this->do_action( 'after_account_connection', $this->_user, $install );
17805 }
17806
17807 $this->switch_to_blog( $current_blog );
17808
17809 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
17810 }
17811 }
17812
17813 /**
17814 * Tries to activate parent account based on add-on's info.
17815 *
17816 * @author Vova Feldman (@svovaf)
17817 * @since 1.2.2.7
17818 *
17819 * @param Freemius $parent_fs
17820 */
17821 private function activate_parent_account( Freemius $parent_fs ) {
17822 if ( ! $this->is_addon() ) {
17823 // This is not an add-on.
17824 return;
17825 }
17826
17827 if ( $parent_fs->is_registered() ) {
17828 // Already activated.
17829 return;
17830 }
17831
17832 // Activate parent with add-on's user credentials.
17833 $parent_install = $this->get_api_user_scope()->call(
17834 "/plugins/{$parent_fs->_plugin->id}/installs.json",
17835 'post',
17836 $parent_fs->get_install_data_for_api( array(
17837 'uid' => $parent_fs->get_anonymous_id(),
17838 ), false, false )
17839 );
17840
17841 if ( isset( $parent_install->error ) ) {
17842 $this->_admin_notices->add(
17843 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17844 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
17845 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17846 'error'
17847 );
17848
17849 return;
17850 }
17851
17852 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
17853
17854 if ( $parent_fs->is_pending_activation() ) {
17855 $parent_fs->_admin_notices->remove_sticky( 'activation_pending' );
17856
17857 unset( $parent_fs->_storage->is_pending_activation );
17858 }
17859
17860 // Get user information based on parent's plugin.
17861 $user = $this->get_user();
17862
17863 // First of all, set site info - otherwise we won't
17864 // be able to invoke API calls.
17865 $parent_fs->_site = new FS_Site( $parent_install );
17866 $parent_fs->_user = $user;
17867
17868 // Sync add-on plans.
17869 $parent_fs->_sync_plans();
17870
17871 $parent_fs->_set_account( $user, $parent_fs->_site );
17872 }
17873
17874 #endregion
17875
17876 #----------------------------------------------------------------------------------
17877 #region Admin Menu Items
17878 #----------------------------------------------------------------------------------
17879
17880 private $_menu_items = array();
17881
17882 /**
17883 * @author Vova Feldman (@svovaf)
17884 * @since 1.2.1.8
17885 *
17886 * @return array
17887 */
17888 function get_menu_items() {
17889 return $this->_menu_items;
17890 }
17891
17892 /**
17893 * @author Vova Feldman (@svovaf)
17894 * @since 1.0.7
17895 *
17896 * @return string
17897 */
17898 function get_menu_slug() {
17899 return $this->_menu->get_slug();
17900 }
17901
17902 /**
17903 * @author Vova Feldman (@svovaf)
17904 * @since 1.0.9
17905 */
17906 function _prepare_admin_menu() {
17907 // if ( ! $this->is_on() ) {
17908 // return;
17909 // }
17910
17911 /**
17912 * When running from a site admin with a network activated module and the connection
17913 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
17914 * site level settings.
17915 *
17916 * @author Vova Feldman (@svovaf)
17917 * @since 2.0.0
17918 */
17919 $should_hide_site_admin_settings = (
17920 $this->_is_network_active &&
17921 ! fs_is_network_admin() &&
17922 ! $this->is_delegated_connection() &&
17923 ! $this->is_anonymous() &&
17924 ! $this->is_registered()
17925 );
17926
17927 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
17928
17929 if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
17930 $should_hide_site_admin_settings
17931 ) {
17932 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
17933 } else {
17934 $this->do_action( fs_is_network_admin() ?
17935 'before_network_admin_menu_init' :
17936 'before_admin_menu_init'
17937 );
17938
17939 $this->add_menu_action();
17940
17941 $this->add_network_menu_when_missing();
17942
17943 $this->add_submenu_items();
17944 }
17945 }
17946
17947 /**
17948 * Admin dashboard menu items modifications.
17949 *
17950 * NOTE: admin_menu action executed before admin_init.
17951 *
17952 * @author Vova Feldman (@svovaf)
17953 * @since 1.0.7
17954 *
17955 */
17956 private function add_menu_action() {
17957 if ( $this->is_activation_mode() ) {
17958 if ( $this->show_opt_in_on_setting_page() ) {
17959 $this->override_plugin_menu_with_activation();
17960 } else {
17961 /**
17962 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
17963 */
17964 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
17965 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
17966 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
17967 fs_request_get_bool( 'pending_activation' )
17968 ) {
17969 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
17970 }
17971 }
17972 } else {
17973 if ( ! $this->is_registered() ) {
17974 // If not registered try to install user.
17975 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
17976 $this->_install_with_new_user();
17977 }
17978 } else if (
17979 fs_request_is_action( 'sync_user' ) &&
17980 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
17981 ) {
17982 $this->_handle_account_user_sync();
17983 }
17984 }
17985 }
17986
17987 /**
17988 * @author Vova Feldman (@svovaf)
17989 * @since 1.0.1
17990 */
17991 function _redirect_on_clicked_menu_link() {
17992 $this->_logger->entrance();
17993
17994 $page = fs_request_get('page');
17995 $page = is_string($page) ? strtolower($page) : '';
17996
17997 $this->_logger->log( 'page = ' . $page );
17998
17999 foreach ( $this->_menu_items as $priority => $items ) {
18000 foreach ( $items as $item ) {
18001 if ( isset( $item['url'] ) ) {
18002 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18003 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18004
18005 fs_redirect( $item['url'] );
18006 }
18007 }
18008 }
18009 }
18010 }
18011
18012 /**
18013 * Remove plugin's all admin menu items & pages, and replace with activation page.
18014 *
18015 * @author Vova Feldman (@svovaf)
18016 * @since 1.0.1
18017 */
18018 private function override_plugin_menu_with_activation() {
18019 $this->_logger->entrance();
18020
18021 $hook = false;
18022
18023 if ( ! $this->has_settings_menu() ) {
18024 // Add the opt-in page without a menu item.
18025 $hook = FS_Admin_Menu_Manager::add_subpage(
18026 null,
18027 $this->get_plugin_name(),
18028 $this->get_plugin_name(),
18029 'manage_options',
18030 $this->_slug,
18031 array( &$this, '_connect_page_render' )
18032 );
18033 } else if ( $this->_menu->is_top_level() ) {
18034 if ( $this->_menu->is_override_exact() ) {
18035 // Make sure the current page is matching the activation page.
18036 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18037 return;
18038 }
18039 }
18040
18041 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18042
18043 if ( false === $hook ) {
18044 // Create new menu item just for the opt-in.
18045 $hook = FS_Admin_Menu_Manager::add_page(
18046 $this->get_plugin_name(),
18047 $this->get_plugin_name(),
18048 'manage_options',
18049 $this->_menu->get_slug(),
18050 array( &$this, '_connect_page_render' )
18051 );
18052 }
18053 } else {
18054 $menus = array( $this->_menu->get_parent_slug() );
18055
18056 if ( $this->_menu->is_override_exact() ) {
18057 // Make sure the current page is matching the activation page.
18058 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18059 return;
18060 }
18061 }
18062
18063 foreach ( $menus as $parent_slug ) {
18064 $hook = $this->_menu->override_submenu_action(
18065 $parent_slug,
18066 $this->_menu->get_raw_slug(),
18067 array( &$this, '_connect_page_render' )
18068 );
18069
18070 if ( false !== $hook ) {
18071 // Found plugin's submenu item.
18072 break;
18073 }
18074 }
18075 }
18076
18077 if ( $this->is_activation_page() ) {
18078 // Clean admin page from distracting content.
18079 self::_clean_admin_content_section();
18080 }
18081
18082 if ( false !== $hook ) {
18083 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18084 $this->_install_with_current_user();
18085 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18086 $this->_install_with_new_user();
18087 }
18088 }
18089 }
18090
18091 /**
18092 * If a plugin was network activated and connected but don't have a network
18093 * level settings, then add an artificial menu item for the Account and other
18094 * Freemius settings.
18095 *
18096 * @author Vova Feldman (@svovaf)
18097 * @since 2.0.0
18098 */
18099 private function add_network_menu_when_missing() {
18100 $this->_logger->entrance();
18101
18102 if ( ! $this->_is_network_active ) {
18103 // Plugin wasn't activated on the network level.
18104 return;
18105 }
18106
18107 if ( ! fs_is_network_admin() ) {
18108 // The context is not the network admin.
18109 return;
18110 }
18111
18112 if ( $this->_menu->has_network_menu() ) {
18113 // Plugin already has a network level menu.
18114 return;
18115 }
18116
18117 if ( $this->is_network_activation_mode() ) {
18118 /**
18119 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18120 * screen is being shown.
18121 *
18122 * @author Leo Fajardo (@leorw)
18123 */
18124 return;
18125 }
18126
18127 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18128 if ( $this->is_network_delegated_connection() ) {
18129 // Super-admin delegated the connection to the site admins.
18130 return;
18131 }
18132 }
18133
18134 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18135
18136 if ( $this->_menu->has_menu() ||
18137 ! $this->is_addon() ||
18138 $this->is_activation_mode()
18139 ) {
18140 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18141 $this->get_plugin_name(),
18142 $this->get_plugin_name(),
18143 'manage_options',
18144 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18145 );
18146 }
18147 } else {
18148 $this->_menu->add_subpage_and_update(
18149 $this->_menu->get_parent_slug(),
18150 $this->get_plugin_name(),
18151 $this->get_plugin_name(),
18152 'manage_options',
18153 $this->_menu->get_slug()
18154 );
18155 }
18156 }
18157
18158 /**
18159 * @author Leo Fajardo (@leorw)
18160 * @since 1.2.1
18161 *
18162 * return string
18163 */
18164 function get_top_level_menu_capability() {
18165 global $menu;
18166
18167 $top_level_menu_slug = $this->get_top_level_menu_slug();
18168
18169 foreach ( $menu as $menu_info ) {
18170 /**
18171 * The second element in the menu info array is the capability/role that has access to the menu and the
18172 * third element is the menu slug.
18173 */
18174 if ( $menu_info[2] === $top_level_menu_slug ) {
18175 return $menu_info[1];
18176 }
18177 }
18178
18179 return 'read';
18180 }
18181
18182 /**
18183 * @author Vova Feldman (@svovaf)
18184 * @since 1.0.0
18185 *
18186 * @return string
18187 */
18188 private function get_top_level_menu_slug() {
18189 return ( $this->is_addon() ?
18190 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18191 $this->_menu->get_top_level_menu_slug() );
18192 }
18193
18194 /**
18195 * @author Vova Feldman (@svovaf)
18196 * @since 1.2.2.7
18197 *
18198 * @return string
18199 */
18200 function get_pricing_cta_label() {
18201 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18202
18203 if ( $this->is_in_trial_promotion() &&
18204 ! $this->is_paying_or_trial()
18205 ) {
18206 // If running a trial promotion, modify the pricing to load the trial.
18207 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18208 } else if ( $this->is_paying() ) {
18209 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18210 }
18211
18212 return $label;
18213 }
18214
18215 /**
18216 * @author Vova Feldman (@svovaf)
18217 * @since 1.2.2.7
18218 *
18219 * @return bool
18220 */
18221 function is_pricing_page_visible() {
18222 return (
18223 // Has at least one paid plan.
18224 $this->has_paid_plan() &&
18225 // Didn't ask to hide the pricing page.
18226 $this->is_page_visible( 'pricing' ) &&
18227 // Don't have a valid active license or has more than one plan.
18228 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18229 );
18230 }
18231
18232 /**
18233 * @author Leo Fajardo (@leorw)
18234 * @since 2.3.0
18235 *
18236 * @param bool $is_activation_mode
18237 *
18238 * @return bool
18239 */
18240 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18241 if ( $this->is_addon() ) {
18242 // No submenu items or action links for add-ons.
18243 return false;
18244 }
18245
18246 if ( $this->show_opt_in_on_themes_page() ) {
18247 if ( ! fs_is_network_admin() ) {
18248 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18249 return true;
18250 }
18251 } else if ( $is_activation_mode ) {
18252 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18253 return false;
18254 }
18255
18256 if ( fs_is_network_admin() ) {
18257 /**
18258 * Add submenu items or action links to network level when plugin was network activated and the super
18259 * admin did NOT delegate the connection of all sites to site admins.
18260 */
18261 return (
18262 $this->_is_network_active &&
18263 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18264 ! $this->is_network_delegated_connection() )
18265 );
18266 }
18267
18268 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18269 }
18270
18271 /**
18272 * Add default Freemius menu items.
18273 *
18274 * @author Vova Feldman (@svovaf)
18275 * @since 1.0.0
18276 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18277 */
18278 private function add_submenu_items() {
18279 $this->_logger->entrance();
18280
18281 $is_activation_mode = $this->is_activation_mode();
18282
18283 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18284
18285 if ( $add_submenu_items ) {
18286 if ( $this->has_affiliate_program() ) {
18287 // Add affiliation page.
18288 $this->add_submenu_item(
18289 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18290 array( &$this, '_affiliation_page_render' ),
18291 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18292 'manage_options',
18293 'affiliation',
18294 'Freemius::_clean_admin_content_section',
18295 WP_FS__DEFAULT_PRIORITY,
18296 $this->is_submenu_item_visible( 'affiliation' )
18297 );
18298 }
18299 }
18300
18301 if ( $add_submenu_items ||
18302 ( $is_activation_mode &&
18303 $this->is_only_premium() &&
18304 $this->is_admin_page( 'account' ) &&
18305 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18306 )
18307 ) {
18308 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18309 $show_account = (
18310 $this->is_submenu_item_visible( 'account' ) &&
18311 /**
18312 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18313 */
18314 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18315 );
18316
18317 // Add user account page.
18318 $this->add_submenu_item(
18319 $this->get_text_inline( 'Account', 'account' ),
18320 array( &$this, '_account_page_render' ),
18321 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18322 'manage_options',
18323 'account',
18324 array( &$this, '_account_page_load' ),
18325 WP_FS__DEFAULT_PRIORITY,
18326 ( $add_submenu_items && $show_account )
18327 );
18328 }
18329 }
18330
18331 if ( $add_submenu_items ) {
18332 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18333 // Add contact page.
18334 $this->add_submenu_item(
18335 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18336 array( &$this, '_contact_page_render' ),
18337 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18338 'manage_options',
18339 'contact',
18340 'Freemius::_clean_admin_content_section',
18341 WP_FS__DEFAULT_PRIORITY,
18342 $this->is_submenu_item_visible( 'contact' )
18343 );
18344 }
18345
18346 if ( $this->has_addons() ) {
18347 $this->add_submenu_item(
18348 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18349 array( &$this, '_addons_page_render' ),
18350 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18351 'manage_options',
18352 'addons',
18353 array( &$this, '_addons_page_load' ),
18354 WP_FS__LOWEST_PRIORITY - 1,
18355 $this->is_submenu_item_visible( 'addons' )
18356 );
18357 }
18358 }
18359
18360 if ( $add_submenu_items ||
18361 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18362 ) {
18363 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18364 $show_pricing = (
18365 $this->is_submenu_item_visible( 'pricing' ) &&
18366 $this->is_pricing_page_visible()
18367 );
18368
18369 $pricing_cta_text = $this->get_pricing_cta_label();
18370 $pricing_class = 'upgrade-mode';
18371 if ( $show_pricing ) {
18372 if ( $this->is_in_trial_promotion() &&
18373 ! $this->is_paying_or_trial()
18374 ) {
18375 // If running a trial promotion, modify the pricing to load the trial.
18376 $pricing_class = 'trial-mode';
18377 } else if ( $this->is_paying() ) {
18378 $pricing_class = '';
18379 }
18380 }
18381
18382 // Add upgrade/pricing page.
18383 $this->add_submenu_item(
18384 $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' ) ),
18385 array( &$this, '_pricing_page_render' ),
18386 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
18387 'manage_options',
18388 'pricing',
18389 'Freemius::_clean_admin_content_section',
18390 WP_FS__LOWEST_PRIORITY,
18391 ( $add_submenu_items && $show_pricing ),
18392 $pricing_class
18393 );
18394 }
18395 }
18396
18397 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
18398 /**
18399 * Add the other menu items if there are any when not in activation mode or license activation is not
18400 * required (license activation is required for registered or anonymous users after activating the
18401 * premium version when the site is not in trial mode or there's no active valid license).
18402 *
18403 * @author Leo Fajardo (@leorw)
18404 * @since 2.2.1
18405 */
18406 if ( 0 < count( $this->_menu_items ) ) {
18407 if ( ! $this->_menu->is_top_level() ) {
18408 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
18409
18410 // Append submenu items right after the plugin's submenu item.
18411 $this->order_sub_submenu_items();
18412 } else {
18413 // Append submenu items.
18414 $this->embed_submenu_items();
18415 }
18416 }
18417 }
18418 }
18419
18420 /**
18421 * Moved the actual submenu item additions to a separated function,
18422 * in order to support sub-submenu items when the plugin's settings
18423 * only have a submenu and not top-level menu item.
18424 *
18425 * @author Vova Feldman (@svovaf)
18426 * @since 1.1.4
18427 */
18428 private function embed_submenu_items() {
18429 $item_template = $this->_menu->is_top_level() ?
18430 '<span class="fs-submenu-item %s %s %s">%s</span>' :
18431 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
18432
18433 $top_level_menu_capability = $this->get_top_level_menu_capability();
18434
18435 ksort( $this->_menu_items );
18436
18437 $is_first_submenu_item = true;
18438
18439 foreach ( $this->_menu_items as $priority => $items ) {
18440 foreach ( $items as $item ) {
18441 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
18442
18443 $menu_item = sprintf(
18444 $item_template,
18445 $this->get_unique_affix(),
18446 $item['menu_slug'],
18447 ! empty( $item['class'] ) ? $item['class'] : '',
18448 $item['menu_title']
18449 );
18450
18451 $top_level_menu_slug = $this->get_top_level_menu_slug();
18452 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
18453
18454 if ( ! isset( $item['url'] ) ) {
18455 $hook = FS_Admin_Menu_Manager::add_subpage(
18456 $item['show_submenu'] ?
18457 $top_level_menu_slug :
18458 null,
18459 $item['page_title'],
18460 $menu_item,
18461 $capability,
18462 $menu_slug,
18463 $item['render_function']
18464 );
18465
18466 if ( false !== $item['before_render_function'] ) {
18467 add_action( "load-$hook", $item['before_render_function'] );
18468 }
18469 } else {
18470 FS_Admin_Menu_Manager::add_subpage(
18471 $item['show_submenu'] ?
18472 $top_level_menu_slug :
18473 null,
18474 $item['page_title'],
18475 $menu_item,
18476 $capability,
18477 $menu_slug,
18478 array( $this, '' )
18479 );
18480 }
18481
18482 if ( $item['show_submenu'] && $is_first_submenu_item ) {
18483 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
18484 /**
18485 * If the top-level menu has been dynamically created, remove the first submenu item that
18486 * WordPress automatically creates when there's no submenu item whose slug matches the
18487 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
18488 *
18489 * Awesome Plugin
18490 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
18491 *
18492 * @author Leo Fajardo (@leorw)
18493 */
18494 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
18495 }
18496
18497 $is_first_submenu_item = false;
18498 }
18499 }
18500 }
18501 }
18502
18503 /**
18504 * Re-order the submenu items so all Freemius added new submenu items
18505 * are added right after the plugin's settings submenu item.
18506 *
18507 * @author Vova Feldman (@svovaf)
18508 * @since 1.1.4
18509 */
18510 private function order_sub_submenu_items() {
18511 global $submenu;
18512
18513 $menu_slug = $this->_menu->get_top_level_menu_slug();
18514
18515 /**
18516 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
18517 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
18518 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
18519 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
18520 *
18521 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
18522 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
18523 * capability needed to access the parent menu as the capability for the submenus that we will add.
18524 */
18525 if ( empty( $submenu[ $menu_slug ] ) ) {
18526 return;
18527 }
18528
18529 $top_level_menu = &$submenu[ $menu_slug ];
18530
18531 $all_submenu_items_after = array();
18532
18533 $found_submenu_item = false;
18534
18535 foreach ( $top_level_menu as $submenu_id => $meta ) {
18536 if ( $found_submenu_item ) {
18537 // Remove all submenu items after the plugin's submenu item.
18538 $all_submenu_items_after[] = $meta;
18539 unset( $top_level_menu[ $submenu_id ] );
18540 }
18541
18542 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
18543 // Found the submenu item, put all below.
18544 $found_submenu_item = true;
18545 continue;
18546 }
18547 }
18548
18549 // Embed all plugin's new submenu items.
18550 $this->embed_submenu_items();
18551
18552 // Start with specially high number to make sure it's appended.
18553 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
18554 foreach ( $all_submenu_items_after as $meta ) {
18555 $top_level_menu[ $i ] = $meta;
18556 $i ++;
18557 }
18558
18559 // Sort submenu items.
18560 ksort( $top_level_menu );
18561 }
18562
18563 /**
18564 * Helper method to return the module's support forum URL.
18565 *
18566 * @author Vova Feldman (@svovaf)
18567 * @since 1.2.2.7
18568 *
18569 * @return string
18570 */
18571 function get_support_forum_url() {
18572 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
18573 }
18574
18575 /**
18576 * Displays the Support Forum link when enabled.
18577 *
18578 * Can be filtered like so:
18579 *
18580 * function _fs_show_support_menu( $is_visible, $menu_id ) {
18581 * if ( 'support' === $menu_id ) {
18582 * return _fs->is_registered();
18583 * }
18584 * return $is_visible;
18585 * }
18586 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
18587 *
18588 */
18589 function _add_default_submenu_items() {
18590 if ( ! $this->is_on() ) {
18591 return;
18592 }
18593
18594 if ( ! $this->is_activation_mode() &&
18595 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
18596 ( ! $this->_is_network_active && is_admin() ) )
18597 ) {
18598 $this->add_submenu_link_item(
18599 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
18600 $this->get_support_forum_url(),
18601 'wp-support-forum',
18602 null,
18603 50,
18604 $this->is_submenu_item_visible( 'support' )
18605 );
18606 }
18607 }
18608
18609 /**
18610 * @author Vova Feldman (@svovaf)
18611 * @since 1.0.1
18612 *
18613 * @param string $menu_title
18614 * @param callable $render_function
18615 * @param bool|string $page_title
18616 * @param string $capability
18617 * @param bool|string $menu_slug
18618 * @param bool|callable $before_render_function
18619 * @param int $priority
18620 * @param bool $show_submenu
18621 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
18622 */
18623 function add_submenu_item(
18624 $menu_title,
18625 $render_function,
18626 $page_title = false,
18627 $capability = 'manage_options',
18628 $menu_slug = false,
18629 $before_render_function = false,
18630 $priority = WP_FS__DEFAULT_PRIORITY,
18631 $show_submenu = true,
18632 $class = ''
18633 ) {
18634 $this->_logger->entrance( 'Title = ' . $menu_title );
18635
18636 if ( $this->is_addon() ) {
18637 $parent_fs = $this->get_parent_instance();
18638
18639 if ( is_object( $parent_fs ) ) {
18640 $parent_fs->add_submenu_item(
18641 $menu_title,
18642 $render_function,
18643 $page_title,
18644 $capability,
18645 $menu_slug,
18646 $before_render_function,
18647 $priority,
18648 $show_submenu,
18649 $class
18650 );
18651
18652 return;
18653 }
18654 }
18655
18656 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
18657 $this->_menu_items[ $priority ] = array();
18658 }
18659
18660 $this->_menu_items[ $priority ][] = array(
18661 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
18662 'menu_title' => $menu_title,
18663 'capability' => $capability,
18664 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
18665 'render_function' => $render_function,
18666 'before_render_function' => $before_render_function,
18667 'show_submenu' => $show_submenu,
18668 'class' => $class,
18669 );
18670 }
18671
18672 /**
18673 * @author Vova Feldman (@svovaf)
18674 * @since 1.0.1
18675 *
18676 * @param string $menu_title
18677 * @param string $url
18678 * @param bool $menu_slug
18679 * @param string $capability
18680 * @param int $priority
18681 * @param bool $show_submenu
18682 */
18683 function add_submenu_link_item(
18684 $menu_title,
18685 $url,
18686 $menu_slug = false,
18687 $capability = 'read',
18688 $priority = WP_FS__DEFAULT_PRIORITY,
18689 $show_submenu = true
18690 ) {
18691 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
18692
18693 if ( $this->is_addon() ) {
18694 $parent_fs = $this->get_parent_instance();
18695
18696 if ( is_object( $parent_fs ) ) {
18697 $parent_fs->add_submenu_link_item(
18698 $menu_title,
18699 $url,
18700 $menu_slug,
18701 $capability,
18702 $priority,
18703 $show_submenu
18704 );
18705
18706 return;
18707 }
18708 }
18709
18710 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
18711 $this->_menu_items[ $priority ] = array();
18712 }
18713
18714 $this->_menu_items[ $priority ][] = array(
18715 'menu_title' => $menu_title,
18716 'capability' => $capability,
18717 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
18718 'url' => $url,
18719 'page_title' => $menu_title,
18720 'render_function' => 'fs_dummy',
18721 'before_render_function' => '',
18722 'show_submenu' => $show_submenu,
18723 );
18724 }
18725
18726 #endregion ------------------------------------------------------------------
18727
18728 #--------------------------------------------------------------------------------
18729 #region Admin Notices
18730 #--------------------------------------------------------------------------------
18731
18732 /**
18733 * @author Vova Feldman (@svovaf)
18734 * @since 2.3.1
18735 *
18736 * @param string|string[] $ids
18737 * @param int|null $network_level_or_blog_id
18738 *
18739 * @uses FS_Admin_Notices::remove_sticky()
18740 */
18741 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
18742 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
18743 }
18744
18745 #endregion
18746
18747 #--------------------------------------------------------------------------------
18748 #region Actions / Hooks / Filters
18749 #--------------------------------------------------------------------------------
18750
18751 /**
18752 * @author Vova Feldman (@svovaf)
18753 * @since 1.1.7
18754 *
18755 * @param string $tag
18756 *
18757 * @return string
18758 */
18759 public function get_action_tag( $tag ) {
18760 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
18761 }
18762
18763 /**
18764 * @author Vova Feldman (@svovaf)
18765 * @since 1.2.1.6
18766 *
18767 * @param string $tag
18768 * @param string $slug
18769 * @param bool $is_plugin
18770 *
18771 * @return string
18772 */
18773 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
18774 $action = "fs_{$tag}";
18775
18776 if ( ! empty( $slug ) ) {
18777 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
18778 }
18779
18780 return $action;
18781 }
18782
18783 /**
18784 * Returns a string that can be used to generate a unique action name,
18785 * option name, HTML element ID, or HTML element class.
18786 *
18787 * @author Leo Fajardo (@leorw)
18788 * @since 1.2.2
18789 *
18790 * @return string
18791 */
18792 public function get_unique_affix() {
18793 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
18794 }
18795
18796 /**
18797 * Returns a string that can be used to generate a unique action name,
18798 * option name, HTML element ID, or HTML element class.
18799 *
18800 * @author Vova Feldman (@svovaf)
18801 * @since 1.2.2.5
18802 *
18803 * @param string $slug
18804 * @param bool $is_plugin
18805 *
18806 * @return string
18807 */
18808 static function get_module_unique_affix( $slug, $is_plugin = true ) {
18809 $affix = $slug;
18810
18811 if ( ! $is_plugin ) {
18812 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
18813 }
18814
18815 return $affix;
18816 }
18817
18818 /**
18819 * @author Vova Feldman (@svovaf)
18820 * @since 1.2.1
18821 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
18822 * based on the slug for backward compatibility.
18823 *
18824 * @param string $tag
18825 *
18826 * @return string
18827 */
18828 function get_ajax_action( $tag ) {
18829 return self::get_ajax_action_static( $tag, $this->_module_id );
18830 }
18831
18832 /**
18833 * @author Vova Feldman (@svovaf)
18834 * @since 1.2.1.7
18835 *
18836 * @param string $tag
18837 *
18838 * @return string
18839 */
18840 function get_ajax_security( $tag ) {
18841 return wp_create_nonce( $this->get_ajax_action( $tag ) );
18842 }
18843
18844 /**
18845 * @author Vova Feldman (@svovaf)
18846 * @since 1.2.1.7
18847 *
18848 * @param string $tag
18849 */
18850 function check_ajax_referer( $tag ) {
18851 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
18852 }
18853
18854 /**
18855 * @author Vova Feldman (@svovaf)
18856 * @since 1.2.1.6
18857 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
18858 * based on the slug for backward compatibility.
18859 *
18860 * @param string $tag
18861 * @param number|null $module_id
18862 *
18863 * @return string
18864 */
18865 private static function get_ajax_action_static( $tag, $module_id = null ) {
18866 $action = "fs_{$tag}";
18867
18868 if ( ! empty( $module_id ) ) {
18869 $action .= "_{$module_id}";
18870 }
18871
18872 return $action;
18873 }
18874
18875 /**
18876 * Do action, specific for the current context plugin.
18877 *
18878 * @author Vova Feldman (@svovaf)
18879 * @since 1.0.1
18880 *
18881 * @param string $tag The name of the action to be executed.
18882 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
18883 * functions hooked to the action. Default empty.
18884 *
18885 * @uses do_action()
18886 */
18887 function do_action( $tag, $arg = '' ) {
18888 $this->_logger->entrance( $tag );
18889
18890 $args = func_get_args();
18891
18892 call_user_func_array( 'do_action', array_merge(
18893 array( $this->get_action_tag( $tag ) ),
18894 array_slice( $args, 1 ) )
18895 );
18896 }
18897
18898 /**
18899 * Add action, specific for the current context plugin.
18900 *
18901 * @author Vova Feldman (@svovaf)
18902 * @since 1.0.1
18903 *
18904 * @param string $tag
18905 * @param callable $function_to_add
18906 * @param int $priority
18907 * @param int $accepted_args
18908 *
18909 * @uses add_action()
18910 */
18911 function add_action(
18912 $tag,
18913 $function_to_add,
18914 $priority = WP_FS__DEFAULT_PRIORITY,
18915 $accepted_args = 1
18916 ) {
18917 $this->_logger->entrance( $tag );
18918
18919 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
18920 }
18921
18922 /**
18923 * Add AJAX action, specific for the current context plugin.
18924 *
18925 * @author Vova Feldman (@svovaf)
18926 * @since 1.2.1
18927 *
18928 * @param string $tag
18929 * @param callable $function_to_add
18930 * @param int $priority
18931 *
18932 * @uses add_action()
18933 *
18934 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
18935 */
18936 function add_ajax_action(
18937 $tag,
18938 $function_to_add,
18939 $priority = WP_FS__DEFAULT_PRIORITY
18940 ) {
18941 $this->_logger->entrance( $tag );
18942
18943 return self::add_ajax_action_static(
18944 $tag,
18945 $function_to_add,
18946 $priority,
18947 $this->_module_id
18948 );
18949 }
18950
18951 /**
18952 * Add AJAX action.
18953 *
18954 * @author Vova Feldman (@svovaf)
18955 * @since 1.2.1.6
18956 *
18957 * @param string $tag
18958 * @param callable $function_to_add
18959 * @param int $priority
18960 * @param number|null $module_id
18961 *
18962 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
18963 * @uses add_action()
18964 *
18965 */
18966 static function add_ajax_action_static(
18967 $tag,
18968 $function_to_add,
18969 $priority = WP_FS__DEFAULT_PRIORITY,
18970 $module_id = null
18971 ) {
18972 self::$_static_logger->entrance( $tag );
18973
18974 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
18975 return false;
18976 }
18977
18978 add_action(
18979 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
18980 $function_to_add,
18981 $priority,
18982 0
18983 );
18984
18985 self::$_static_logger->info( "$tag AJAX callback action added." );
18986
18987 return true;
18988 }
18989
18990 /**
18991 * Send a JSON response back to an Ajax request.
18992 *
18993 * @author Vova Feldman (@svovaf)
18994 * @since 1.2.1.5
18995 *
18996 * @param mixed $response
18997 */
18998 static function shoot_ajax_response( $response ) {
18999 wp_send_json( $response );
19000 }
19001
19002 /**
19003 * Send a JSON response back to an Ajax request, indicating success.
19004 *
19005 * @author Vova Feldman (@svovaf)
19006 * @since 1.2.1.5
19007 *
19008 * @param mixed $data Data to encode as JSON, then print and exit.
19009 */
19010 static function shoot_ajax_success( $data = null ) {
19011 wp_send_json_success( $data );
19012 }
19013
19014 /**
19015 * Send a JSON response back to an Ajax request, indicating failure.
19016 *
19017 * @author Vova Feldman (@svovaf)
19018 * @since 1.2.1.5
19019 *
19020 * @param mixed $error Optional error message.
19021 */
19022 static function shoot_ajax_failure( $error = '' ) {
19023 $result = array( 'success' => false );
19024 if ( ! empty( $error ) ) {
19025 $result['error'] = $error;
19026 }
19027
19028 wp_send_json( $result );
19029 }
19030
19031 /**
19032 * Apply filter, specific for the current context plugin.
19033 *
19034 * @author Vova Feldman (@svovaf)
19035 * @since 1.0.9
19036 *
19037 * @param string $tag The name of the filter hook.
19038 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19039 *
19040 * @return mixed The filtered value after all hooked functions are applied to it.
19041 *
19042 * @uses apply_filters()
19043 */
19044 function apply_filters( $tag, $value ) {
19045 $this->_logger->entrance( $tag );
19046
19047 $args = func_get_args();
19048 array_unshift( $args, $this->get_unique_affix() );
19049
19050 return call_user_func_array( 'fs_apply_filter', $args );
19051 }
19052
19053 /**
19054 * Add filter, specific for the current context plugin.
19055 *
19056 * @author Vova Feldman (@svovaf)
19057 * @since 1.0.9
19058 *
19059 * @param string $tag
19060 * @param callable $function_to_add
19061 * @param int $priority
19062 * @param int $accepted_args
19063 *
19064 * @uses add_filter()
19065 */
19066 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19067 $this->_logger->entrance( $tag );
19068
19069 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19070 }
19071
19072 /**
19073 * Check if has filter.
19074 *
19075 * @author Vova Feldman (@svovaf)
19076 * @since 1.1.4
19077 *
19078 * @param string $tag
19079 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19080 *
19081 * @return false|int
19082 *
19083 * @uses has_filter()
19084 */
19085 function has_filter( $tag, $function_to_check = false ) {
19086 $this->_logger->entrance( $tag );
19087
19088 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19089 }
19090
19091 #endregion
19092
19093 /**
19094 * Override default i18n text phrases.
19095 *
19096 * @author Vova Feldman (@svovaf)
19097 * @since 1.1.6
19098 *
19099 * @param string[] string $key_value
19100 *
19101 * @uses fs_override_i18n()
19102 */
19103 function override_i18n( $key_value ) {
19104 fs_override_i18n( $key_value, $this->_slug );
19105 }
19106
19107 /* Account Page
19108 ------------------------------------------------------------------------------------------------------------------*/
19109 /**
19110 * Update site information.
19111 *
19112 * @author Vova Feldman (@svovaf)
19113 * @since 1.0.1
19114 *
19115 * @param bool $store Flush to Database if true.
19116 * @param null|int $network_level_or_blog_id Since 2.0.0
19117 * @param \FS_Site $site Since 2.0.0
19118 */
19119 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null ) {
19120 $this->_logger->entrance();
19121
19122 if ( is_null( $site ) ) {
19123 $site = $this->_site;
19124 }
19125
19126 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19127 $this->_logger->error( "Empty install ID, can't store site." );
19128
19129 return;
19130 }
19131
19132 $site_clone = clone $site;
19133
19134 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id );
19135
19136 if ( is_object( $this->_user ) && $this->_user->id != $site->user_id ) {
19137 $this->sync_user_by_current_install( $site->user_id );
19138
19139 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19140
19141 if ( empty( $prev_stored_user_id ) &&
19142 is_object($this->_user) && $this->_user->id != $site->user_id
19143 ) {
19144 /**
19145 * Store the current user ID as the previous user ID so that the previous user can be used
19146 * as the install's owner while the new owner's details are not yet available.
19147 *
19148 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19149 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19150 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19151 * into `replica`.
19152 *
19153 * @author Leo Fajardo (@leorw)
19154 */
19155 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19156 }
19157 }
19158
19159 $sites[ $this->_slug ] = $site_clone;
19160
19161 $this->set_account_option( 'sites', $sites, $store, $network_level_or_blog_id );
19162 }
19163
19164 /**
19165 * Update plugin's plans information.
19166 *
19167 * @author Vova Feldman (@svovaf)
19168 * @since 1.0.2
19169 *
19170 * @param bool $store Flush to Database if true.
19171 */
19172 private function _store_plans( $store = true ) {
19173 $this->_logger->entrance();
19174
19175 $plans = self::get_all_plans( $this->_module_type );
19176
19177 // Copy plans.
19178 $encrypted_plans = array();
19179 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19180 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19181 }
19182
19183 $plans[ $this->_slug ] = $encrypted_plans;
19184
19185 $this->set_account_option( 'plans', $plans, $store );
19186 }
19187
19188 /**
19189 * Update user's plugin licenses.
19190 *
19191 * @author Vova Feldman (@svovaf)
19192 * @since 1.0.5
19193 *
19194 * @param bool $store
19195 * @param number|bool $module_id
19196 * @param FS_Plugin_License[] $licenses
19197 */
19198 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19199 $this->_logger->entrance();
19200
19201 $all_licenses = self::get_all_licenses();
19202
19203 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19204 $module_id = $this->_module_id;
19205
19206 $user_licenses = is_array( $this->_licenses ) ?
19207 $this->_licenses :
19208 array();
19209
19210 if ( empty( $user_licenses ) ) {
19211 // If the context user doesn't have any license, don't update the licenses collection.
19212 return;
19213 }
19214
19215 $new_user_licenses_map = array();
19216 foreach ( $user_licenses as $user_license ) {
19217 $new_user_licenses_map[ $user_license->id ] = $user_license;
19218 }
19219
19220 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19221
19222 // Update user licenses.
19223 $licenses_to_update_count = count( $new_user_licenses_map );
19224 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19225 if ( 0 === $licenses_to_update_count ) {
19226 break;
19227 }
19228
19229 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19230 // Update license.
19231 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19232 unset( $new_user_licenses_map[ $license->id ] );
19233
19234 $licenses_to_update_count --;
19235 }
19236 }
19237
19238 if ( ! empty( $new_user_licenses_map ) ) {
19239 // Add new licenses.
19240 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19241 }
19242
19243 $licenses = $all_licenses[ $module_id ];
19244 }
19245
19246 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19247 $all_licenses[ $module_id ] = array();
19248 }
19249
19250 $all_licenses[ $module_id ] = $licenses;
19251
19252 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19253 }
19254
19255 /**
19256 * Update user information.
19257 *
19258 * @author Vova Feldman (@svovaf)
19259 * @since 1.0.1
19260 *
19261 * @param bool $store Flush to Database if true.
19262 */
19263 private function _store_user( $store = true ) {
19264 $this->_logger->entrance();
19265
19266 if ( empty( $this->_user->id ) ) {
19267 $this->_logger->error( "Empty user ID, can't store user." );
19268
19269 return;
19270 }
19271
19272 $users = self::get_all_users();
19273 $users[ $this->_user->id ] = $this->_user;
19274 self::$_accounts->set_option( 'users', $users, $store );
19275 }
19276
19277 /**
19278 * Update new updates information.
19279 *
19280 * @author Vova Feldman (@svovaf)
19281 * @since 1.0.4
19282 *
19283 * @param FS_Plugin_Tag|null $update
19284 * @param bool $store Flush to Database if true.
19285 * @param bool|number $plugin_id
19286 */
19287 private function _store_update( $update, $store = true, $plugin_id = false ) {
19288 $this->_logger->entrance();
19289
19290 if ( $update instanceof FS_Plugin_Tag ) {
19291 $update->updated = time();
19292 }
19293
19294 if ( ! is_numeric( $plugin_id ) ) {
19295 $plugin_id = $this->_plugin->id;
19296 }
19297
19298 $updates = self::get_all_updates();
19299 $updates[ $plugin_id ] = $update;
19300 self::$_accounts->set_option( 'updates', $updates, $store );
19301 }
19302
19303 /**
19304 * Update new updates information.
19305 *
19306 * @author Vova Feldman (@svovaf)
19307 * @since 1.0.6
19308 *
19309 * @param FS_Plugin[] $plugin_addons
19310 * @param bool $store Flush to Database if true.
19311 */
19312 private function _store_addons( $plugin_addons, $store = true ) {
19313 $this->_logger->entrance();
19314
19315 $addons = self::get_all_addons();
19316 $addons[ $this->_plugin->id ] = $plugin_addons;
19317 self::$_accounts->set_option( 'addons', $addons, $store );
19318 }
19319
19320 /**
19321 * Delete plugin's associated add-ons.
19322 *
19323 * @author Vova Feldman (@svovaf)
19324 * @since 1.0.8
19325 *
19326 * @param bool $store
19327 *
19328 * @return bool
19329 */
19330 private function _delete_account_addons( $store = true ) {
19331 $all_addons = self::get_all_account_addons();
19332
19333 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
19334 return false;
19335 }
19336
19337 unset( $all_addons[ $this->_plugin->id ] );
19338
19339 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19340
19341 return true;
19342 }
19343
19344 /**
19345 * Update account add-ons list.
19346 *
19347 * @author Vova Feldman (@svovaf)
19348 * @since 1.0.6
19349 *
19350 * @param FS_Plugin[] $addons
19351 * @param bool $store Flush to Database if true.
19352 */
19353 private function _store_account_addons( $addons, $store = true ) {
19354 $this->_logger->entrance();
19355
19356 $all_addons = self::get_all_account_addons();
19357 $all_addons[ $this->_plugin->id ] = $addons;
19358 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19359 }
19360
19361 /**
19362 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
19363 * the valid user licenses will be fetched again and the account add-ons may be updated.
19364 *
19365 * @author Leo Fajardo (@leorw)
19366 * @since 2.2.4
19367 */
19368 private function purge_valid_user_licenses_cache() {
19369 if ( ! $this->is_registered() ) {
19370 return;
19371 }
19372
19373 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
19374 }
19375
19376 /**
19377 * @author Leo Fajardo (@leorw)
19378 * @since 2.3.0
19379 *
19380 * @param array $all_licenses
19381 * @param number|null $site_license_id
19382 * @param bool $include_parent_licenses
19383 *
19384 * @return array
19385 */
19386 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
19387 $foreign_licenses = array(
19388 'ids' => array(),
19389 'license_keys' => array()
19390 );
19391
19392 $parent_license_ids_map = array();
19393
19394 foreach ( $all_licenses as $license ) {
19395 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
19396 continue;
19397 }
19398
19399 $foreign_licenses['ids'][] = $license->id;
19400 $foreign_licenses['license_keys'][] = $license->secret_key;
19401
19402 if (
19403 $include_parent_licenses &&
19404 is_object( $this->_license ) &&
19405 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
19406 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
19407 ) {
19408 /**
19409 * Include the parent license's info only if it has not been included before since child licenses
19410 * can have the same parent license.
19411 */
19412 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
19413 $foreign_licenses['license_keys'][] = $license->secret_key;
19414
19415 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
19416 }
19417 }
19418
19419 if ( empty( $foreign_licenses['ids'] ) ) {
19420 $foreign_licenses = array();
19421 }
19422
19423 return $foreign_licenses;
19424 }
19425
19426 /**
19427 * @author Leo Fajardo (@leorw)
19428 * @since 2.3.0
19429 *
19430 * @return string
19431 */
19432 private function get_valid_user_licenses_endpoint() {
19433 $user_licenses_endpoint = '/licenses.json?type=active' .
19434 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
19435
19436 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
19437
19438 if ( ! empty ( $foreign_licenses ) ) {
19439 $foreign_licenses = array(
19440 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
19441 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
19442 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
19443 );
19444
19445 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
19446 }
19447
19448 return $user_licenses_endpoint;
19449 }
19450
19451 /**
19452 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
19453 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
19454 * method filters out non–add-on product IDs and stores the add-on IDs.
19455 *
19456 * @author Leo Fajardo (@leorw)
19457 * @since 2.2.4
19458 *
19459 * @return stdClass[] array
19460 */
19461 private function fetch_valid_user_licenses() {
19462 $this->_logger->entrance();
19463
19464 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
19465
19466 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
19467 ! is_array( $result->licenses )
19468 ) {
19469 return array();
19470 }
19471
19472 return $result->licenses;
19473 }
19474
19475 /**
19476 * @author Leo Fajardo (@leorw)
19477 * @since 2.2.4
19478 *
19479 * @return number[] Account add-on IDs.
19480 */
19481 function get_updated_account_addons() {
19482 $addons = $this->get_addons();
19483 if ( empty( $addons ) ) {
19484 return array();
19485 }
19486
19487 $account_addons = $this->get_account_addons();
19488 if ( ! is_array( $account_addons ) ) {
19489 $account_addons = array();
19490 }
19491
19492 $user_licenses = $this->is_registered() ?
19493 $this->fetch_valid_user_licenses() :
19494 array();
19495
19496 if ( empty( $user_licenses ) ) {
19497 return $account_addons;
19498 }
19499
19500 $addon_ids = array();
19501 foreach ( $addons as $addon ) {
19502 $addon_ids[] = $addon->id;
19503 }
19504
19505 $license_product_ids = array();
19506
19507 foreach ( $user_licenses as $license ) {
19508 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
19509 $license_product_ids = array_merge( $license_product_ids, $license->products );
19510 } else {
19511 $license_product_ids[] = $license->plugin_id;
19512 }
19513 }
19514
19515 // Filter out non–add-on IDs.
19516 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
19517 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
19518 $this->_store_account_addons( array_unique( $new_account_addons ) );
19519 }
19520
19521 return $new_account_addons;
19522 }
19523
19524 /**
19525 * Store account params in the Database.
19526 *
19527 * @author Vova Feldman (@svovaf)
19528 * @since 1.0.1
19529 *
19530 * @param null|int $blog_id Since 2.0.0
19531 */
19532 private function _store_account( $blog_id = null ) {
19533 $this->_logger->entrance();
19534
19535 $this->_store_site( false, $blog_id );
19536 $this->_store_user( false );
19537 $this->_store_plans( false );
19538 $this->_store_licenses( false );
19539
19540 self::$_accounts->store( $blog_id );
19541 }
19542
19543 /**
19544 * Sync user's information.
19545 *
19546 * @author Vova Feldman (@svovaf)
19547 * @since 1.0.3
19548 * @uses FS_Api
19549 */
19550 private function _handle_account_user_sync() {
19551 $this->_logger->entrance();
19552
19553 $api = $this->get_api_user_scope();
19554
19555 // Get user's information.
19556 $user = $api->get( '/', true );
19557
19558 if ( isset( $user->id ) ) {
19559 $this->_user->first = $user->first;
19560 $this->_user->last = $user->last;
19561 $this->_user->email = $user->email;
19562
19563 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
19564
19565 if ( $user->is_verified &&
19566 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
19567 ) {
19568 $this->_user->is_verified = true;
19569
19570 $this->do_action( 'account_email_verified', $user->email );
19571
19572 $this->_admin_notices->add(
19573 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
19574 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
19575 'success',
19576 // Make admin sticky if account menu item is invisible,
19577 // since the page will be auto redirected to the plugin's
19578 // main settings page, and the non-sticky message
19579 // will disappear.
19580 ! $is_menu_item_account_visible,
19581 'email_verified'
19582 );
19583 }
19584
19585 // Flush user details to DB.
19586 $this->_store_user();
19587
19588 $this->do_action( 'after_account_user_sync', $user );
19589
19590 /**
19591 * If account menu item is hidden, redirect to plugin's main settings page.
19592 *
19593 * @author Vova Feldman (@svovaf)
19594 * @since 1.1.6
19595 *
19596 * @link https://github.com/Freemius/wordpress-sdk/issues/6
19597 */
19598 if ( ! $is_menu_item_account_visible ) {
19599 fs_redirect( $this->_get_admin_page_url() );
19600 }
19601 }
19602 }
19603
19604 /**
19605 * @author Vova Feldman (@svovaf)
19606 * @since 1.0.9
19607 * @uses FS_Api
19608 *
19609 * @param number|bool $license_id
19610 *
19611 * @return FS_Subscription|object|bool
19612 */
19613 private function _fetch_site_license_subscription( $license_id = false ) {
19614 $this->_logger->entrance();
19615 $api = $this->get_api_site_scope();
19616
19617 if ( ! is_numeric( $license_id ) ) {
19618 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
19619 $this->_license->parent_license_id :
19620 $this->_license->id;
19621 }
19622
19623 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
19624
19625 return ! isset( $result->error ) ?
19626 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
19627 new FS_Subscription( $result->subscriptions[0] ) :
19628 false
19629 ) :
19630 $result;
19631 }
19632
19633 /**
19634 * @author Vova Feldman (@svovaf)
19635 * @since 1.0.4
19636 * @uses FS_Api
19637 *
19638 * @param number|bool $plan_id
19639 *
19640 * @return FS_Plugin_Plan|object
19641 */
19642 private function _fetch_site_plan( $plan_id = false ) {
19643 $this->_logger->entrance();
19644 $api = $this->get_api_site_scope();
19645
19646 if ( ! is_numeric( $plan_id ) ) {
19647 $plan_id = $this->_site->plan_id;
19648 }
19649
19650 $plan = $api->get( "/plans/{$plan_id}.json", true );
19651
19652 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
19653 }
19654
19655 /**
19656 * @author Vova Feldman (@svovaf)
19657 * @since 1.0.5
19658 * @uses FS_Api
19659 *
19660 * @return FS_Plugin_Plan[]|object
19661 */
19662 private function _fetch_plugin_plans() {
19663 $this->_logger->entrance();
19664 $api = $this->get_current_or_network_user_api_scope();
19665
19666 /**
19667 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
19668 */
19669 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
19670
19671 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
19672 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
19673 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
19674 }
19675
19676 $result = $result->plans;
19677 }
19678
19679 return $result;
19680 }
19681
19682 /**
19683 * @author Vova Feldman (@svovaf)
19684 * @since 2.0.0
19685 *
19686 * @param number $plan_id
19687 *
19688 * @return \FS_Plugin_Plan|object
19689 */
19690 private function fetch_plan_by_id( $plan_id ) {
19691 $this->_logger->entrance();
19692 $api = $this->get_current_or_network_user_api_scope();
19693
19694 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
19695
19696 return $this->is_api_result_entity( $result ) ?
19697 new FS_Plugin_Plan( $result ) :
19698 $result;
19699 }
19700
19701 /**
19702 * @author Vova Feldman (@svovaf)
19703 * @since 1.0.5
19704 * @uses FS_Api
19705 *
19706 * @param number|bool $plugin_id
19707 * @param number|bool $site_license_id
19708 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
19709 * @param number|null $blog_id
19710 *
19711 * @return FS_Plugin_License[]|object
19712 */
19713 private function _fetch_licenses(
19714 $plugin_id = false,
19715 $site_license_id = false,
19716 $foreign_licenses = array(),
19717 $blog_id = null
19718 ) {
19719 $this->_logger->entrance();
19720
19721 $api = $this->get_api_user_scope();
19722
19723 if ( ! is_numeric( $plugin_id ) ) {
19724 $plugin_id = $this->_plugin->id;
19725 }
19726
19727 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
19728 if ( ! empty ( $foreign_licenses ) ) {
19729 $foreign_licenses = array(
19730 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
19731 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
19732 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
19733 );
19734
19735 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
19736 }
19737
19738 $result = $api->get( $user_licenses_endpoint, true );
19739
19740 $is_site_license_synced = false;
19741
19742 $api_errors = array();
19743
19744 if ( $this->is_api_result_object( $result, 'licenses' ) &&
19745 is_array( $result->licenses )
19746 ) {
19747 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
19748 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
19749
19750 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
19751 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
19752 }
19753 }
19754
19755 $result = $result->licenses;
19756 } else {
19757 $api_errors[] = $result;
19758 $result = array();
19759 }
19760
19761 if ( ! $is_site_license_synced ) {
19762 if ( ! is_null( $blog_id ) ) {
19763 /**
19764 * If blog ID is not null, the request is for syncing of the license of a single site via the
19765 * network-level "Account" page.
19766 *
19767 * @author Leo Fajardo (@leorw)
19768 */
19769 $this->switch_to_blog( $blog_id );
19770 }
19771
19772 $api = $this->get_api_site_scope();
19773
19774 if ( is_numeric( $site_license_id ) ) {
19775 // Try to retrieve a foreign license that is linked to the install.
19776 $api_result = $api->call( '/licenses.json?is_enriched=true' );
19777
19778 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
19779 is_array( $api_result->licenses )
19780 ) {
19781 $licenses = $api_result->licenses;
19782
19783 if ( ! empty( $licenses ) ) {
19784 $result[] = new FS_Plugin_License( $licenses[0] );
19785 }
19786 } else {
19787 $api_errors[] = $api_result;
19788 }
19789 } else if (
19790 is_object( $this->_license ) &&
19791 /**
19792 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
19793 * the FS instance that does the syncing is the parent FS instance.
19794 *
19795 * @author Leo Fajardo (@leorw)
19796 * @since 2.3.0
19797 */
19798 $this->_license->plugin_id == $plugin_id
19799 ) {
19800 $is_license_in_result = false;
19801 if ( ! empty( $result ) ) {
19802 foreach ( $result as $license ) {
19803 if ( $license->id == $this->_license->id ) {
19804 $is_license_in_result = true;
19805 break;
19806 }
19807 }
19808 }
19809
19810 if ( ! $is_license_in_result ) {
19811 // Fetch foreign license by ID and license key.
19812 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
19813 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
19814
19815 if ( $this->is_api_result_entity( $license ) ) {
19816 $result[] = new FS_Plugin_License( $license );
19817 } else {
19818 $api_errors[] = $license;
19819 }
19820 }
19821 }
19822
19823 if ( ! is_null( $blog_id ) ) {
19824 $this->switch_to_blog( $this->_storage->network_install_blog_id );
19825 }
19826 }
19827
19828 if ( is_array( $result ) && 0 < count( $result ) ) {
19829 // If found at least one license, return license collection even if there are errors.
19830 return $result;
19831 }
19832
19833 if ( ! empty( $api_errors ) ) {
19834 // If found any errors and no licenses, return first error.
19835 return $api_errors[0];
19836 }
19837
19838 // Fallback to empty licenses list.
19839 return $result;
19840 }
19841
19842 /**
19843 * @author Vova Feldman (@svovaf)
19844 * @since 2.0.0
19845 *
19846 * @param number $license_id
19847 * @param string $license_key
19848 *
19849 * @return \FS_Plugin_License|object
19850 */
19851 private function fetch_license_by_key( $license_id, $license_key ) {
19852 $this->_logger->entrance();
19853
19854 $api = $this->get_current_or_network_user_api_scope();
19855
19856 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
19857
19858 return $this->is_api_result_entity( $result ) ?
19859 new FS_Plugin_License( $result ) :
19860 $result;
19861 }
19862
19863 /**
19864 * @author Vova Feldman (@svovaf)
19865 * @since 1.2.0
19866 * @uses FS_Api
19867 *
19868 * @param number|bool $plugin_id
19869 * @param bool $flush
19870 *
19871 * @return FS_Payment[]|object
19872 */
19873 function _fetch_payments( $plugin_id = false, $flush = false ) {
19874 $this->_logger->entrance();
19875
19876 $api = $this->get_api_user_scope();
19877
19878 if ( ! is_numeric( $plugin_id ) ) {
19879 $plugin_id = $this->_plugin->id;
19880 }
19881
19882 $include_bundles = (
19883 is_object( $this->_plugin ) &&
19884 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
19885 );
19886
19887 $result = $api->get(
19888 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
19889 $flush
19890 );
19891
19892 if ( ! isset( $result->error ) ) {
19893 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
19894 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
19895 }
19896 $result = $result->payments;
19897 }
19898
19899 return $result;
19900 }
19901
19902 /**
19903 * @author Vova Feldman (@svovaf)
19904 * @since 1.2.1.5
19905 * @uses FS_Api
19906 *
19907 * @param bool $flush
19908 *
19909 * @return \FS_Billing|mixed
19910 */
19911 function _fetch_billing( $flush = false ) {
19912 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
19913
19914 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
19915
19916 if ( $this->is_api_result_entity( $billing ) ) {
19917 $billing = new FS_Billing( $billing );
19918 }
19919
19920 return $billing;
19921 }
19922
19923 /**
19924 * @author Vova Feldman (@svovaf)
19925 * @since 1.0.5
19926 *
19927 * @param FS_Plugin_License[] $licenses
19928 * @param number $module_id
19929 */
19930 private function _update_licenses( $licenses, $module_id ) {
19931 $this->_logger->entrance();
19932
19933 if ( is_array( $licenses ) ) {
19934 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
19935 $licenses[ $i ]->updated = time();
19936 }
19937 }
19938
19939 $this->_store_licenses( true, $module_id, $licenses );
19940 }
19941
19942 /**
19943 * @author Vova Feldman (@svovaf)
19944 * @since 1.0.4
19945 *
19946 * @param bool|number $plugin_id
19947 * @param bool $flush Since 1.1.7.3
19948 * @param int $expiration Since 1.2.2.7
19949 * @param bool|string $newer_than Since 2.2.1
19950 *
19951 * @return object|false New plugin tag info if exist.
19952 */
19953 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
19954 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
19955
19956 if ( ! is_object( $latest_tag ) ) {
19957 return false;
19958 }
19959
19960 $plugin_version = $this->get_plugin_version();
19961
19962 // Check if version is actually newer.
19963 $has_new_version =
19964 // If it's an non-installed add-on then always return latest.
19965 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
19966 // Compare versions.
19967 version_compare( $plugin_version, $latest_tag->version, '<' );
19968
19969 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
19970
19971 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
19972
19973 $this->_storage->beta_data = array(
19974 'is_beta' => $is_latest_version_beta,
19975 'version' => $latest_tag->version
19976 );
19977
19978 return $has_new_version ? $latest_tag : false;
19979 }
19980
19981 /**
19982 * @author Vova Feldman (@svovaf)
19983 * @since 1.0.5
19984 *
19985 * @param bool|number $plugin_id
19986 * @param bool $flush Since 1.1.7.3
19987 * @param int $expiration Since 1.2.2.7
19988 * @param bool|string $newer_than Since 2.2.1
19989 *
19990 * @return bool|FS_Plugin_Tag
19991 */
19992 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
19993 $this->_logger->entrance();
19994
19995 if ( ! is_numeric( $plugin_id ) ) {
19996 $plugin_id = $this->_plugin->id;
19997 }
19998
19999 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20000 $updates = $this->get_all_updates();
20001
20002 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20003 }
20004
20005 /**
20006 * Check if site assigned with active license.
20007 *
20008 * @author Vova Feldman (@svovaf)
20009 * @since 1.0.6
20010 *
20011 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20012 */
20013 function has_active_license() {
20014 return (
20015 is_object( $this->_license ) &&
20016 is_numeric( $this->_license->id ) &&
20017 ! $this->_license->is_expired()
20018 );
20019 }
20020
20021 /**
20022 * Check if site assigned with active & valid (not expired) license.
20023 *
20024 * @author Vova Feldman (@svovaf)
20025 * @since 1.2.1
20026 *
20027 * @param bool $check_expiration
20028 */
20029 function has_active_valid_license( $check_expiration = true ) {
20030 return self::is_active_valid_license( $this->_license, $check_expiration );
20031 }
20032
20033 /**
20034 * @author Leo Fajardo (@leorw)
20035 * @since 2.3.1
20036 */
20037 function is_data_debug_mode() {
20038 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20039 return false;
20040 }
20041
20042 $fs = $this->is_addon() ?
20043 $this->get_parent_instance() :
20044 $this;
20045
20046 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20047 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20048 } else {
20049 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20050 }
20051
20052 return ( 'true' === $is_developer_license_debug_mode );
20053 }
20054
20055 /**
20056 * @author Leo Fajardo (@leorw)
20057 * @since 2.3.1
20058 */
20059 function _set_data_debug_mode() {
20060 if ( ! $this->is_whitelabeled( true ) ) {
20061 return;
20062 }
20063
20064 $license_or_user_key = fs_request_get( 'license_or_user_key' );
20065
20066 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20067 'true' :
20068 'false';
20069
20070 if ( 'true' === $transient_value ) {
20071 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20072 'last_license_key' :
20073 'last_license_user_key'
20074 );
20075
20076 if ( md5( $license_or_user_key ) !== $stored_key ) {
20077 $this->shoot_ajax_failure( sprintf(
20078 '%s... %s',
20079 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20080 $this->get_text_inline(
20081 'seems like the key you entered doesn\'t match our records.',
20082 'developer-or-license-not-found'
20083 )
20084 ) );
20085 }
20086 }
20087
20088 if ( $this->is_network_active() && fs_is_network_admin() ) {
20089 set_site_transient(
20090 "fs_{$this->get_id()}_data_debug_mode",
20091 $transient_value,
20092 WP_FS__TIME_24_HOURS_IN_SEC / 24
20093 );
20094 } else {
20095 set_transient(
20096 "fs_{$this->get_id()}_data_debug_mode",
20097 $transient_value,
20098 WP_FS__TIME_24_HOURS_IN_SEC / 24
20099 );
20100 }
20101
20102 if ( 'true' === $transient_value ) {
20103 $this->_admin_notices->add_sticky(
20104 $this->get_text_inline(
20105 '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.',
20106 'data_debug_mode_enabled'
20107 ),
20108 'data_debug_mode_enabled'
20109 );
20110 }
20111
20112 $this->shoot_ajax_success();
20113 }
20114
20115 /**
20116 * Check if a given license is active & valid (not expired).
20117 *
20118 * @author Vova Feldman (@svovaf)
20119 * @since 2.1.3
20120 *
20121 * @param FS_Plugin_License $license
20122 * @param bool $check_expiration
20123 *
20124 * @return bool
20125 */
20126 private static function is_active_valid_license( $license, $check_expiration = true ) {
20127 return (
20128 is_object( $license ) &&
20129 FS_Plugin_License::is_valid_id( $license->id ) &&
20130 $license->is_active() &&
20131 ( ! $check_expiration || $license->is_valid() )
20132 );
20133 }
20134
20135 /**
20136 * Checks if there's any site that is associated with an active & valid license.
20137 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20138 *
20139 * @author Vova Feldman (@svovaf)
20140 * @since 2.1.3
20141 *
20142 * @return bool
20143 */
20144 function has_any_active_valid_license() {
20145 if ( ! fs_is_network_admin() ) {
20146 return $this->has_active_valid_license();
20147 }
20148
20149 $installs = $this->get_blog_install_map();
20150 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20151
20152 foreach ( $installs as $blog_id => $install ) {
20153 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20154 continue;
20155 }
20156
20157 foreach ( $all_plugin_licenses as $license ) {
20158 if ( $license->id == $install->license_id ) {
20159 if ( self::is_active_valid_license( $license ) ) {
20160 return true;
20161 }
20162 }
20163 }
20164 }
20165
20166 return false;
20167 }
20168
20169 /**
20170 * Check if site assigned with license with enabled features.
20171 *
20172 * @author Vova Feldman (@svovaf)
20173 * @since 1.0.6
20174 *
20175 * @return bool
20176 */
20177 function has_features_enabled_license() {
20178 return (
20179 is_object( $this->_license ) &&
20180 is_numeric( $this->_license->id ) &&
20181 $this->_license->is_features_enabled()
20182 );
20183 }
20184
20185 /**
20186 * Checks if the product is activated with a bundle license.
20187 *
20188 * @author Leo Fajardo (@leorw)
20189 * @since 2.4.0
20190 *
20191 * @return bool
20192 */
20193 function is_activated_with_bundle_license() {
20194 if ( ! $this->has_features_enabled_license() ) {
20195 return false;
20196 }
20197
20198 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20199 }
20200
20201 /**
20202 * Check if user is a trial or have feature enabled license.
20203 *
20204 * @author Vova Feldman (@svovaf)
20205 * @since 1.1.7
20206 *
20207 * @return bool
20208 */
20209 function can_use_premium_code() {
20210 return $this->is_trial() || $this->has_features_enabled_license();
20211 }
20212
20213 /**
20214 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20215 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20216 * the context user is authenticated.
20217 *
20218 * @author Leo Fajardo (@leorw)
20219 * @since 1.2.2
20220 *
20221 * @return bool
20222 */
20223 function is_user_admin() {
20224 /**
20225 * Require a super-admin when network activated, running from the network level OR if
20226 * running from the site level but not delegated the opt-in.
20227 *
20228 * @author Vova Feldman (@svovaf)
20229 * @since 2.0.0
20230 */
20231 if ( $this->_is_network_active &&
20232 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20233 ) {
20234 return is_super_admin();
20235 }
20236
20237 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20238 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20239 }
20240
20241 /**
20242 * Sync site's plan.
20243 *
20244 * @author Vova Feldman (@svovaf)
20245 * @since 1.0.3
20246 *
20247 * @uses FS_Api
20248 *
20249 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20250 * the admin.
20251 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20252 * network-level "Account" page.
20253 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20254 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20255 * updates for a single site in case `execute_cron` has switched to a different blog.
20256 */
20257 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20258 $this->_logger->entrance();
20259
20260 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20261
20262 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20263
20264 if ( $is_addon_sync ) {
20265 $this->_sync_addon_license( $plugin_id, $background );
20266 } else {
20267 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20268 }
20269
20270 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20271 }
20272
20273 /**
20274 * Sync plugin's add-on license.
20275 *
20276 * @author Vova Feldman (@svovaf)
20277 * @since 1.0.6
20278 * @uses FS_Api
20279 *
20280 * @param number $addon_id
20281 * @param bool $background
20282 */
20283 private function _sync_addon_license( $addon_id, $background ) {
20284 $this->_logger->entrance();
20285
20286 if ( $this->is_addon_activated( $addon_id ) ) {
20287 // If already installed, use add-on sync.
20288 $fs_addon = self::get_instance_by_id( $addon_id );
20289
20290 if (
20291 // Add-on is network activated and network integrated.
20292 $fs_addon->is_network_active() ||
20293 // Background sync cron.
20294 self::is_cron() ||
20295 // Add-on is not network activated or not network integrated.
20296 ! fs_is_network_admin()
20297 ) {
20298 $fs_addon->_sync_license( $background );
20299
20300 return;
20301 }
20302 }
20303
20304 // Validate add-on exists.
20305 $addon = $this->get_addon( $addon_id );
20306
20307 if ( ! is_object( $addon ) ) {
20308 return;
20309 }
20310
20311 // Add add-on into account add-ons.
20312 $account_addons = $this->get_account_addons();
20313 if ( ! is_array( $account_addons ) ) {
20314 $account_addons = array();
20315 }
20316 $account_addons[] = $addon->id;
20317 $account_addons = array_unique( $account_addons );
20318 $this->_store_account_addons( $account_addons );
20319
20320 // Load add-on licenses.
20321 $licenses = $this->_fetch_licenses( $addon->id );
20322
20323 // Sync add-on licenses.
20324 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
20325 $this->_update_licenses( $licenses, $addon->id );
20326
20327 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
20328 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
20329
20330 if ( ! isset( $plans_result->error ) ) {
20331 $plans = array();
20332 foreach ( $plans_result->plans as $plan ) {
20333 $plans[] = new FS_Plugin_Plan( $plan );
20334 }
20335
20336 $this->_admin_notices->add_sticky(
20337 sprintf(
20338 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
20339 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
20340 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
20341 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
20342 $addon->title
20343 ) . ' ' . $this->get_latest_download_link(
20344 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
20345 $addon_id
20346 ),
20347 'addon_plan_upgraded_' . $addon->slug,
20348 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20349 );
20350 }
20351 }
20352 }
20353 }
20354
20355 /**
20356 * Sync site's plugin plan.
20357 *
20358 * @author Vova Feldman (@svovaf)
20359 * @since 1.0.6
20360 * @uses FS_Api
20361 *
20362 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
20363 * @param bool $send_installs_update Since 2.0.0
20364 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
20365 * syncing its license from the network-level "Account" page (e.g.: after
20366 * activating a license only for the single install).
20367 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
20368 * can be used here to switch to the previous blog in case `execute_cron`
20369 * has switched to a different blog.
20370 */
20371 private function _sync_plugin_license(
20372 $background = false,
20373 $send_installs_update = true,
20374 $is_context_single_site = false,
20375 $current_blog_id = null
20376 ) {
20377 $this->_logger->entrance();
20378
20379 $plan_change = 'none';
20380
20381 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
20382
20383 if ( ! $send_installs_update ) {
20384 $site = $this->_site;
20385 } else {
20386 /**
20387 * Sync site info.
20388 *
20389 * @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.
20390 */
20391 if ( $is_site_level_sync ) {
20392 /**
20393 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
20394 *
20395 * @author Leo Fajardo (@leorw)
20396 * @since 2.2.3
20397 */
20398 if ( is_numeric( $current_blog_id ) ) {
20399 $this->switch_to_blog( $current_blog_id );
20400 }
20401
20402 $result = $this->send_install_update( array(), true );
20403 $is_valid = $this->is_api_result_entity( $result );
20404 } else {
20405 $result = $this->send_installs_update( array(), true );
20406 $is_valid = $this->is_api_result_object( $result, 'installs' );
20407 }
20408
20409 if ( ! $is_valid ) {
20410 if ( $is_context_single_site ) {
20411 // Switch back to the main blog so that the following logic will have the right entities.
20412 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20413 }
20414
20415 // Show API messages only if not background sync or if paying customer.
20416 if ( ! $background || $this->is_paying() ) {
20417 // Try to ping API to see if not blocked.
20418 if ( ! FS_Api::test() ) {
20419 /**
20420 * Failed to ping API - blocked!
20421 *
20422 * @author Vova Feldman (@svovaf)
20423 * @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.
20424 */
20425 $api = $this->get_api_site_scope();
20426
20427 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
20428 self::$_global_admin_notices->add(
20429 sprintf(
20430 $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' ),
20431 $this->get_plugin_name(),
20432 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
20433 'api.freemius.com',
20434 'wp.freemius.com'
20435 ) ) ) . '</b>'
20436 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
20437 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
20438 'error',
20439 $background,
20440 'api_blocked'
20441 );
20442 }
20443 } else {
20444 // Authentication params are broken.
20445 $this->_admin_notices->add(
20446 $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 ),
20447 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
20448 'error'
20449 );
20450 }
20451 }
20452
20453 // No reason to continue with license sync while there are API issues.
20454 return;
20455 }
20456
20457 if ( $is_site_level_sync ) {
20458 $site = new FS_Site( $result );
20459 } else {
20460 // Map site addresses to their blog IDs.
20461 $address_to_blog_map = $this->get_address_to_blog_map();
20462
20463 // Find the current context install.
20464 $site = null;
20465 foreach ( $result->installs as $install ) {
20466 if ( $install->id == $this->_site->id ) {
20467 $site = new FS_Site( $install );
20468 } else {
20469 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
20470 $blog_id = $address_to_blog_map[ $address ];
20471
20472 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
20473 }
20474 }
20475 }
20476
20477 // Sync plans.
20478 $this->_sync_plans();
20479 }
20480
20481 // Remove sticky API connectivity message.
20482 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
20483
20484 if ( ! $this->has_paid_plan() ) {
20485 $this->_site = $site;
20486 $this->_store_site(
20487 true,
20488 $is_site_level_sync ?
20489 null :
20490 $this->get_network_install_blog_id()
20491 );
20492 } else {
20493 $context_blog_id = 0;
20494
20495 if ( $is_context_single_site ) {
20496 $context_blog_id = get_current_blog_id();
20497
20498 // Switch back to the main blog in order to properly sync the license.
20499 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20500 }
20501
20502 /**
20503 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
20504 * associated with that ID is not included in the user's licenses collection.
20505 */
20506 $this->_sync_licenses(
20507 $site->license_id,
20508 ( $is_context_single_site ?
20509 $context_blog_id :
20510 null
20511 )
20512 );
20513
20514 if ( $is_context_single_site ) {
20515 $this->switch_to_blog( $context_blog_id );
20516 }
20517
20518 // Check if plan / license changed.
20519 if ( $site->plan_id != $this->_site->plan_id ||
20520 // Check if trial started.
20521 $site->trial_plan_id != $this->_site->trial_plan_id ||
20522 $site->trial_ends != $this->_site->trial_ends ||
20523 // Check if license changed.
20524 $site->license_id != $this->_site->license_id
20525 ) {
20526 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
20527 // New trial started.
20528 $this->_site = $site;
20529 $plan_change = 'trial_started';
20530
20531 // For trial with subscription use-case.
20532 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
20533
20534 if ( is_object( $new_license ) && $new_license->is_valid() ) {
20535 $this->_site = $site;
20536 $this->_update_site_license( $new_license );
20537 $this->_store_licenses();
20538
20539 $this->_sync_site_subscription( $this->_license );
20540 }
20541 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
20542 // Was in trial, but now trial expired and no license ID.
20543 // New trial started.
20544 $this->_site = $site;
20545 $plan_change = 'trial_expired';
20546 } else {
20547 $is_free = $this->is_free_plan();
20548
20549 // Make sure license exist and not expired.
20550 $new_license = is_null( $site->license_id ) ?
20551 null :
20552 $this->_get_license_by_id( $site->license_id );
20553
20554 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
20555 // License cancelled.
20556 $this->_site = $site;
20557 $this->_update_site_license( $new_license );
20558 $this->_store_licenses();
20559
20560 $plan_change = 'cancelled';
20561 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
20562 // The license is expired, so ignore upgrade method.
20563 $this->_site = $site;
20564 } else {
20565 // License changed.
20566 $this->_site = $site;
20567
20568 /**
20569 * IMPORTANT:
20570 * 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.
20571 *
20572 * @author Vova Feldman (@svovaf)
20573 * @since 2.0.0
20574 */
20575 $this->_update_site_license( $new_license );
20576
20577 if ( ! $is_context_single_site &&
20578 fs_is_network_admin() &&
20579 $this->_is_network_active &&
20580 $new_license->quota > 1 &&
20581 get_blog_count() > 1
20582 ) {
20583 // See if license can activated on all sites.
20584 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
20585 if ( ! fs_request_get_bool( 'auto_install' ) ) {
20586 // Open the license activation dialog box on the account page.
20587 add_action( 'admin_footer', array(
20588 &$this,
20589 '_open_license_activation_dialog_box'
20590 ) );
20591 }
20592 }
20593 }
20594
20595 $this->_store_licenses();
20596
20597 $plan_change = $is_free ?
20598 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
20599 ( is_object( $new_license ) ?
20600 'changed' :
20601 'downgraded' );
20602 }
20603 }
20604
20605 // Store updated site info.
20606 $this->_store_site(
20607 true,
20608 $is_site_level_sync ?
20609 null :
20610 $this->get_network_install_blog_id()
20611 );
20612 } else {
20613 if ( ! is_object( $this->_license ) ) {
20614 $this->maybe_update_whitelabel_flag(
20615 FS_Plugin_License::is_valid_id( $site->license_id ) ?
20616 $this->get_license_by_id( $site->license_id ) :
20617 null
20618 );
20619 } else {
20620 $this->maybe_update_whitelabel_flag( $this->_license );
20621
20622 if ( $this->_license->is_expired() ) {
20623 if ( ! $this->has_features_enabled_license() ) {
20624 $this->_deactivate_license();
20625 $plan_change = 'downgraded';
20626 } else {
20627 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
20628
20629 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
20630 /**
20631 * Show the expired license notice every 14 days.
20632 *
20633 * @author Leo Fajardo (@leorw)
20634 * @since 2.3.1
20635 */
20636 $plan_change = 'expired';
20637 }
20638 }
20639 }
20640 }
20641
20642 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
20643 $this->_sync_site_subscription( $this->_license );
20644 }
20645 }
20646
20647 if ( ! $this->is_addon() &&
20648 $this->_site->is_beta() !== $site->is_beta
20649 ) {
20650 // Beta flag updated.
20651 $this->_site = $site;
20652
20653 $this->_store_site(
20654 true,
20655 $is_site_level_sync ?
20656 null :
20657 $this->get_network_install_blog_id()
20658 );
20659 }
20660
20661 if ( $this->is_addon() || $this->has_addons() ) {
20662 /**
20663 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
20664 * an updated valid user licenses collection will be fetched from the server which is used to also
20665 * update the account add-ons (add-ons the user has licenses for).
20666 *
20667 * @author Leo Fajardo (@leorw)
20668 * @since 2.2.4
20669 */
20670 $this->purge_valid_user_licenses_cache();
20671 }
20672 }
20673
20674 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
20675
20676 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
20677 switch ( $plan_change ) {
20678 case 'none':
20679 if ( ! $background && is_admin() ) {
20680 $plan = $this->is_trial() ?
20681 $this->get_trial_plan() :
20682 $this->get_plan();
20683
20684 if ( $plan->is_free() ) {
20685 $this->_admin_notices->add(
20686 sprintf(
20687 $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' ),
20688 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
20689 ) . ' ' . sprintf(
20690 '<a href="%s">%s</a>',
20691 $this->contact_url(
20692 'bug',
20693 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' ),
20694 strtoupper( $plan->name )
20695 )
20696 ),
20697 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
20698 ),
20699 $hmm_text
20700 );
20701 }
20702 }
20703 break;
20704 case 'upgraded':
20705 case 'activated':
20706 $this->_admin_notices->add_sticky(
20707 ( 'activated' === $plan_change ) ?
20708 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ) :
20709 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) .
20710 $this->get_complete_upgrade_instructions(),
20711 'plan_upgraded',
20712 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20713 );
20714
20715 $this->_admin_notices->remove_sticky( array(
20716 'trial_started',
20717 'trial_promotion',
20718 'trial_expired',
20719 'activation_complete',
20720 'license_expired',
20721 ) );
20722 break;
20723 case 'changed':
20724 $this->_admin_notices->add_sticky(
20725 sprintf(
20726 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
20727 $this->get_plan_title()
20728 ),
20729 'plan_changed'
20730 );
20731
20732 $this->_admin_notices->remove_sticky( array(
20733 'trial_started',
20734 'trial_promotion',
20735 'trial_expired',
20736 'activation_complete',
20737 ) );
20738 break;
20739 case 'downgraded':
20740 $this->_admin_notices->add_sticky(
20741 ($this->has_free_plan() ?
20742 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 ) :
20743 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
20744 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) ) ),
20745 'license_expired',
20746 $hmm_text
20747 );
20748 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20749 break;
20750 case 'cancelled':
20751 $this->_admin_notices->add(
20752 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
20753 sprintf(
20754 '<a href="%s">%s</a>',
20755 $this->contact_url( 'bug' ),
20756 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
20757 ),
20758 $hmm_text,
20759 'error'
20760 );
20761 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20762 break;
20763 case 'expired':
20764 $this->_admin_notices->add_sticky(
20765 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 ),
20766 'license_expired',
20767 $hmm_text
20768 );
20769
20770 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
20771
20772 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20773 break;
20774 case 'trial_started':
20775 $this->_admin_notices->add_sticky(
20776 sprintf(
20777 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
20778 '<i>' . $this->get_plugin_name() . '</i>'
20779 ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ),
20780 'trial_started',
20781 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20782 );
20783
20784 $this->_admin_notices->remove_sticky( array(
20785 'trial_promotion',
20786 ) );
20787 break;
20788 case 'trial_expired':
20789 $this->_admin_notices->add_sticky(
20790 ($this->has_free_plan() ?
20791 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
20792 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
20793 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))),
20794 'trial_expired',
20795 $hmm_text
20796 );
20797 $this->_admin_notices->remove_sticky( array(
20798 'trial_started',
20799 'trial_promotion',
20800 'plan_upgraded',
20801 ) );
20802 break;
20803 }
20804 }
20805
20806 if ( 'none' !== $plan_change ) {
20807 if (
20808 ! is_object( $this->_license ) ||
20809 ! $this->_license->is_whitelabeled
20810 ) {
20811 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
20812 }
20813
20814 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
20815 }
20816 }
20817
20818 /**
20819 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
20820 *
20821 * @author Vova Feldman (@svovaf)
20822 * @since 2.0.0
20823 */
20824 public function _open_license_activation_dialog_box() {
20825 $vars = array( 'license_id' => $this->_site->license_id );
20826 fs_require_once_template( 'js/open-license-activation.php', $vars );
20827 }
20828
20829 /**
20830 * @author Vova Feldman (@svovaf)
20831 * @since 1.0.5
20832 *
20833 * @param bool $background
20834 * @param FS_Plugin_License|null $premium_license
20835 */
20836 protected function _activate_license( $background = false, $premium_license = null ) {
20837 $this->_logger->entrance();
20838
20839 if ( is_null( $premium_license ) ) {
20840 $license_id = fs_request_get( 'license_id' );
20841
20842 if ( is_object( $this->_site ) &&
20843 FS_Plugin_License::is_valid_id( $license_id ) &&
20844 $license_id == $this->_site->license_id
20845 ) {
20846 // License is already activated.
20847 return;
20848 }
20849
20850 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
20851 $this->_get_license_by_id( $license_id ) :
20852 $this->_get_available_premium_license();
20853 }
20854
20855 if ( ! is_object( $premium_license ) ) {
20856 return;
20857 }
20858
20859 if ( ! is_object( $this->_site ) ) {
20860 // Not yet opted-in.
20861 $user = $this->get_current_or_network_user();
20862 if ( ! is_object( $user ) ) {
20863 $user = self::_get_user_by_id( $premium_license->user_id );
20864 }
20865
20866 if ( is_object( $user ) ) {
20867 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
20868 } else {
20869 $this->opt_in(
20870 false,
20871 false,
20872 false,
20873 $premium_license->secret_key
20874 );
20875
20876 return;
20877 }
20878 }
20879
20880
20881 /**
20882 * If the premium license is already associated with the install, just
20883 * update the license reference (activation is not required).
20884 *
20885 * @since 1.1.9
20886 */
20887 if ( $premium_license->id == $this->_site->license_id ) {
20888 // License is already activated.
20889 $this->_update_site_license( $premium_license );
20890 $this->_store_account();
20891
20892 return;
20893 }
20894
20895 if ( $this->_site->user_id != $premium_license->user_id ) {
20896 $api_request_params = array( 'license_key' => $premium_license->secret_key );
20897 } else {
20898 $api_request_params = array();
20899 }
20900
20901 $api = $this->get_api_site_scope();
20902 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
20903
20904 if ( ! $this->is_api_result_entity( $license ) ) {
20905 if ( ! $background ) {
20906 $this->_admin_notices->add( sprintf(
20907 '%s %s',
20908 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
20909 ( is_object( $license ) && isset( $license->error ) ?
20910 $license->error->message :
20911 sprintf( '%s<br><code>%s</code>',
20912 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
20913 var_export( $license, true )
20914 )
20915 )
20916 ),
20917 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
20918 'error'
20919 );
20920 }
20921
20922 return;
20923 }
20924
20925 $premium_license = new FS_Plugin_License( $license );
20926
20927 // Updated site plan.
20928 $site = $this->get_api_site_scope()->get( '/', true );
20929 if ( $this->is_api_result_entity( $site ) ) {
20930 $this->_site = new FS_Site( $site );
20931 }
20932 $this->_update_site_license( $premium_license );
20933
20934 $this->_store_account();
20935
20936 if ( $this->is_addon() || $this->has_addons() ) {
20937 /**
20938 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
20939 * an updated valid user licenses collection will be fetched from the server which is used to also
20940 * update the account add-ons (add-ons the user has licenses for).
20941 *
20942 * @author Leo Fajardo (@leorw)
20943 * @since 2.2.4
20944 */
20945 $this->purge_valid_user_licenses_cache();
20946 }
20947
20948 if ( ! $background ) {
20949 $this->_admin_notices->add_sticky(
20950 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
20951 $this->get_complete_upgrade_instructions(),
20952 'license_activated',
20953 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20954 );
20955 }
20956
20957 $this->_admin_notices->remove_sticky( array(
20958 'trial_promotion',
20959 'license_expired',
20960 ) );
20961 }
20962
20963 /**
20964 * @author Vova Feldman (@svovaf)
20965 * @since 1.0.5
20966 *
20967 * @param bool $show_notice
20968 */
20969 protected function _deactivate_license( $show_notice = true ) {
20970 $this->_logger->entrance();
20971
20972 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
20973
20974 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
20975 $this->_admin_notices->add(
20976 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() ),
20977 $hmm_text
20978 );
20979
20980 return;
20981 }
20982
20983 $api = $this->get_api_site_scope();
20984 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
20985
20986 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
20987 }
20988
20989 /**
20990 * @author Leo Fajardo (@leorw)
20991 * @since 2.2.1
20992 *
20993 * @param FS_Plugin_License $license
20994 * @param bool|string $hmm_text
20995 * @param bool $show_notice
20996 */
20997 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
20998 if ( isset( $license->error ) ) {
20999 $this->_admin_notices->add(
21000 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21001 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21002 $hmm_text,
21003 'error'
21004 );
21005
21006 return;
21007 }
21008
21009 // Update license cache.
21010 if ( is_array( $this->_licenses ) ) {
21011 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21012 if ( $license->id == $this->_licenses[ $i ]->id ) {
21013 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21014 }
21015 }
21016 }
21017
21018 // Update site plan to default.
21019 $this->_sync_plans();
21020 $this->_site->plan_id = $this->_plans[0]->id;
21021 // Unlink license from site.
21022 $this->_update_site_license( null );
21023
21024 $this->_store_account();
21025
21026 if ( $show_notice ) {
21027 $this->_admin_notices->add(
21028 sprintf( $this->is_only_premium() ?
21029 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21030 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21031 $this->get_plan_title()
21032 ),
21033 $this->get_text_inline( 'O.K', 'ok' )
21034 );
21035 }
21036
21037 $this->_admin_notices->remove_sticky( array(
21038 'plan_upgraded',
21039 'license_activated',
21040 ) );
21041 }
21042
21043 /**
21044 * Site plan downgrade.
21045 *
21046 * @author Vova Feldman (@svovaf)
21047 * @since 1.0.4
21048 *
21049 * @return object
21050 *
21051 * @uses FS_Api
21052 */
21053 private function _downgrade_site() {
21054 $this->_logger->entrance();
21055
21056 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21057
21058 $api = $this->get_api_site_scope();
21059 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21060
21061 $plan_downgraded = false;
21062 $plan = false;
21063 if ( $this->is_api_result_entity( $site ) ) {
21064 $prev_plan_id = $this->_site->plan_id;
21065
21066 // Update new site plan id.
21067 $this->_site->plan_id = $site->plan_id;
21068
21069 $plan = $this->get_plan();
21070 $subscription = $this->_sync_site_subscription( $this->_license );
21071
21072 // Plan downgraded if plan was changed or subscription was cancelled.
21073 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21074 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21075 } else {
21076 // handle different error cases.
21077 $this->handle_license_deactivation_result(
21078 $site,
21079 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21080 );
21081 }
21082
21083 if ( ! $plan_downgraded ) {
21084 return (object) array(
21085 'error' => (object) array(
21086 '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' )
21087 )
21088 );
21089 }
21090
21091 // Remove previous sticky message about upgrade (if exist).
21092 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21093
21094 $this->_admin_notices->add(
21095 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21096 $plan->title,
21097 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21098 )
21099 );
21100
21101 // Store site updates.
21102 $this->_store_site();
21103
21104 if ( $deactivate_license &&
21105 ! FS_Plugin_License::is_valid_id( $site->license_id )
21106 ) {
21107 if ( $this->_site->is_localhost() ) {
21108 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21109 } else {
21110 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21111 }
21112
21113 // Handle successful license deactivation result.
21114 $this->handle_license_deactivation_result( $this->_license );
21115 }
21116
21117 return $site;
21118 }
21119
21120 /**
21121 * @author Vova Feldman (@svovaf)
21122 * @since 1.1.8.1
21123 *
21124 * @param bool|string $plan_name
21125 *
21126 * @return bool If trial was successfully started.
21127 */
21128 function start_trial( $plan_name = false ) {
21129 $this->_logger->entrance();
21130
21131 // Alias.
21132 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21133
21134 if ( $this->is_trial() ) {
21135 // Already in trial mode.
21136 $this->_admin_notices->add(
21137 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21138 $oops_text,
21139 'error'
21140 );
21141
21142 return false;
21143 }
21144
21145 if ( $this->_site->is_trial_utilized() ) {
21146 // Trial was already utilized.
21147 $this->_admin_notices->add(
21148 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21149 $oops_text,
21150 'error'
21151 );
21152
21153 return false;
21154 }
21155
21156 if ( false !== $plan_name ) {
21157 $plan = $this->get_plan_by_name( $plan_name );
21158
21159 if ( false === $plan ) {
21160 // Plan doesn't exist.
21161 $this->_admin_notices->add(
21162 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21163 $oops_text,
21164 'error'
21165 );
21166
21167 return false;
21168 }
21169
21170 if ( ! $plan->has_trial() ) {
21171 // Plan doesn't exist.
21172 $this->_admin_notices->add(
21173 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21174 $oops_text,
21175 'error'
21176 );
21177
21178 return false;
21179 }
21180 } else {
21181 if ( ! $this->has_trial_plan() ) {
21182 // None of the plans have a trial.
21183 $this->_admin_notices->add(
21184 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21185 $oops_text,
21186 'error'
21187 );
21188
21189 return false;
21190 }
21191
21192 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21193
21194 $plan = $plans_with_trial[0];
21195 }
21196
21197 $api = $this->get_api_site_scope();
21198 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
21199
21200 if ( ! $this->is_api_result_entity( $plan ) ) {
21201 // Some API error while trying to start the trial.
21202 $this->_admin_notices->add(
21203 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type )
21204 . ' ' . var_export( $plan, true ),
21205 $oops_text,
21206 'error'
21207 );
21208
21209 return false;
21210 }
21211
21212 // Sync license.
21213 $this->_sync_license();
21214
21215 return $this->is_trial();
21216 }
21217
21218 /**
21219 * Cancel site trial.
21220 *
21221 * @author Vova Feldman (@svovaf)
21222 * @since 1.0.9
21223 *
21224 * @return object
21225 *
21226 * @uses FS_Api
21227 */
21228 private function _cancel_trial() {
21229 $this->_logger->entrance();
21230
21231 if ( ! $this->is_trial() ) {
21232 return (object) array(
21233 'error' => (object) array(
21234 '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' )
21235 )
21236 );
21237 }
21238
21239 $trial_plan = $this->get_trial_plan();
21240
21241 $api = $this->get_api_site_scope();
21242 $site = $api->call( 'trials.json', 'delete' );
21243
21244 $trial_cancelled = false;
21245
21246 if ( $this->is_api_result_entity( $site ) ) {
21247 $prev_trial_ends = $this->_site->trial_ends;
21248
21249 if ( $this->is_paid_trial() ) {
21250 $this->_license->expiration = $site->trial_ends;
21251 $this->_license->is_cancelled = true;
21252 $this->_update_site_license( $this->_license );
21253 $this->_store_licenses();
21254
21255 // Clear subscription reference.
21256 $this->_sync_site_subscription( null );
21257 }
21258
21259 // Update site info.
21260 $this->_site = new FS_Site( $site );
21261
21262 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
21263 } else {
21264 // @todo handle different error cases.
21265 }
21266
21267 if ( ! $trial_cancelled ) {
21268 return (object) array(
21269 'error' => (object) array(
21270 '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' )
21271 )
21272 );
21273 }
21274
21275 // Remove previous sticky messages about upgrade or trial (if exist).
21276 $this->_admin_notices->remove_sticky( array(
21277 'trial_started',
21278 'trial_promotion',
21279 'plan_upgraded',
21280 ) );
21281
21282 // Store site updates.
21283 $this->_store_site();
21284
21285 if ( ! $this->is_addon() ||
21286 ! $this->deactivate_premium_only_addon_without_license( true )
21287 ) {
21288 $this->_admin_notices->add(
21289 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
21290 );
21291 }
21292
21293 return $site;
21294 }
21295
21296 /**
21297 * @author Vova Feldman (@svovaf)
21298 * @since 1.0.6
21299 *
21300 * @param bool|number $plugin_id
21301 *
21302 * @return bool
21303 */
21304 private function _is_addon_id( $plugin_id ) {
21305 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
21306 }
21307
21308 /**
21309 * Check if user eligible to download premium version updates.
21310 *
21311 * @author Vova Feldman (@svovaf)
21312 * @since 1.0.6
21313 *
21314 * @return bool
21315 */
21316 private function _can_download_premium() {
21317 return $this->has_any_active_valid_license() ||
21318 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
21319 }
21320
21321 /**
21322 *
21323 * @author Vova Feldman (@svovaf)
21324 * @since 1.0.6
21325 *
21326 * @param bool|number $addon_id
21327 * @param string $type "json" or "zip"
21328 *
21329 * @return string
21330 */
21331 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
21332
21333 $is_addon = $this->_is_addon_id( $addon_id );
21334
21335 $is_premium = null;
21336 if ( ! $is_addon ) {
21337 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
21338 } else if ( $this->is_addon_activated( $addon_id ) ) {
21339 $fs_addon = self::get_instance_by_id( $addon_id );
21340 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
21341 }
21342
21343 // If add-on, then append add-on ID.
21344 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
21345 '/updates/latest.' . $type;
21346
21347 // If add-on and not yet activated, try to fetch based on server licensing.
21348 if ( is_bool( $is_premium ) ) {
21349 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
21350 }
21351
21352 if ( $this->has_secret_key() ) {
21353 $endpoint = add_query_arg( 'type', 'all', $endpoint );
21354 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
21355 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
21356 }
21357
21358 return $endpoint;
21359 }
21360
21361 /**
21362 * @author Vova Feldman (@svovaf)
21363 * @since 1.0.4
21364 *
21365 * @param bool|number $addon_id
21366 * @param bool $flush Since 1.1.7.3
21367 * @param int $expiration Since 1.2.2.7
21368 * @param bool|string $newer_than Since 2.2.1
21369 * @param bool|string $fetch_readme Since 2.2.1
21370 *
21371 * @return object|false Plugin latest tag info.
21372 */
21373 function _fetch_latest_version(
21374 $addon_id = false,
21375 $flush = true,
21376 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
21377 $newer_than = false,
21378 $fetch_readme = true
21379 ) {
21380 $this->_logger->entrance();
21381
21382 $switch_to_blog_id = null;
21383
21384 /**
21385 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
21386 * @since 1.1.7.4 Also check updates for add-ons.
21387 */
21388 if ( ! $this->is_registered() &&
21389 ! $this->_is_addon_id( $addon_id )
21390 ) {
21391 if ( ! is_multisite() ) {
21392 return false;
21393 }
21394
21395 $installs_map = $this->get_blog_install_map();
21396
21397 foreach ( $installs_map as $blog_id => $install ) {
21398 /**
21399 * @var FS_Site $install
21400 */
21401 if ( $install->is_trial() ) {
21402 $switch_to_blog_id = $blog_id;
21403 break;
21404 }
21405
21406 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
21407 $license = $this->get_license_by_id( $install->license_id );
21408
21409 if ( is_object( $license ) && $license->is_features_enabled() ) {
21410 $switch_to_blog_id = $blog_id;
21411 break;
21412 }
21413 }
21414 }
21415
21416 if ( is_null( $switch_to_blog_id ) ) {
21417 return false;
21418 }
21419 }
21420
21421 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
21422 get_current_blog_id() :
21423 0;
21424
21425 if ( is_numeric( $switch_to_blog_id ) ) {
21426 $this->switch_to_blog( $switch_to_blog_id );
21427 }
21428
21429 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
21430
21431 if ( ! empty( $newer_than ) ) {
21432 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
21433 }
21434
21435 if ( true === $fetch_readme ) {
21436 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
21437 }
21438
21439 $tag = $this->get_api_site_or_plugin_scope()->get(
21440 $latest_version_endpoint,
21441 $flush,
21442 $expiration
21443 );
21444
21445 if ( is_numeric( $switch_to_blog_id ) ) {
21446 $this->switch_to_blog( $current_blog_id );
21447 }
21448
21449 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
21450
21451 $this->_logger->departure( 'Latest version ' . $latest_version );
21452
21453 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
21454 }
21455
21456 #----------------------------------------------------------------------------------
21457 #region Download Plugin
21458 #----------------------------------------------------------------------------------
21459
21460 /**
21461 * Download latest plugin version, based on plan.
21462 *
21463 * Not like _download_latest(), this will redirect the page
21464 * to secure download url to prevent dual download (from FS to WP server,
21465 * and then from WP server to the client / browser).
21466 *
21467 * @author Vova Feldman (@svovaf)
21468 * @since 1.0.9
21469 *
21470 * @param bool|number $plugin_id
21471 *
21472 * @uses FS_Api
21473 * @uses wp_redirect()
21474 */
21475 private function download_latest_directly( $plugin_id = false ) {
21476 $this->_logger->entrance();
21477
21478 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
21479 }
21480
21481 /**
21482 * Get latest plugin FS API download URL.
21483 *
21484 * @author Vova Feldman (@svovaf)
21485 * @since 1.0.9
21486 *
21487 * @param bool|number $plugin_id
21488 *
21489 * @return string
21490 */
21491 private function get_latest_download_api_url( $plugin_id = false ) {
21492 $this->_logger->entrance();
21493
21494 return $this->get_api_site_scope()->get_signed_url(
21495 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
21496 );
21497 }
21498
21499 /**
21500 * Get payment invoice URL.
21501 *
21502 * @author Vova Feldman (@svovaf)
21503 * @since 1.2.0
21504 *
21505 * @param bool|number $payment_id
21506 *
21507 * @return string
21508 */
21509 function _get_invoice_api_url( $payment_id = false ) {
21510 $this->_logger->entrance();
21511
21512 $url = $this->get_api_user_scope()->get_signed_url(
21513 "/payments/{$payment_id}/invoice.pdf"
21514 );
21515
21516 if ( ! fs_starts_with( $url, 'https://' ) ) {
21517 // Always use HTTPS for invoices.
21518 $url = 'https' . substr( $url, 4 );
21519 }
21520
21521 return $url;
21522 }
21523
21524 /**
21525 * Get latest plugin download link.
21526 *
21527 * @author Vova Feldman (@svovaf)
21528 * @since 1.0.9
21529 *
21530 * @param string $label
21531 * @param bool|number $plugin_id
21532 *
21533 * @return string
21534 */
21535 private function get_latest_download_link( $label, $plugin_id = false ) {
21536 return sprintf(
21537 '<a target="_blank" rel="noopener" href="%s">%s</a>',
21538 $this->_get_latest_download_local_url( $plugin_id ),
21539 $label
21540 );
21541 }
21542
21543 /**
21544 * Get latest plugin download local URL.
21545 *
21546 * @author Vova Feldman (@svovaf)
21547 * @since 1.0.9
21548 *
21549 * @param bool|number $plugin_id
21550 *
21551 * @return string
21552 */
21553 function _get_latest_download_local_url( $plugin_id = false ) {
21554 // Add timestamp to protect from caching.
21555 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
21556
21557 if ( ! empty( $plugin_id ) ) {
21558 $params['plugin_id'] = $plugin_id;
21559 } else if ( $this->is_addon() ) {
21560 $params['plugin_id'] = $this->get_id();
21561 }
21562
21563 $fs = $this->is_addon() ?
21564 $this->get_parent_instance() :
21565 $this;
21566
21567 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
21568 }
21569
21570 #endregion Download Plugin ------------------------------------------------------------------
21571
21572 /**
21573 * @author Vova Feldman (@svovaf)
21574 * @since 1.0.4
21575 *
21576 * @uses FS_Api
21577 *
21578 * @param bool $background Hints the method if it's a background updates check. If false, it means that
21579 * was initiated by the admin.
21580 * @param bool|number $plugin_id
21581 * @param bool $flush Since 1.1.7.3
21582 * @param int $expiration Since 1.2.2.7
21583 * @param bool|string $newer_than Since 2.2.1
21584 */
21585 private function check_updates(
21586 $background = false,
21587 $plugin_id = false,
21588 $flush = true,
21589 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
21590 $newer_than = false
21591 ) {
21592 $this->_logger->entrance();
21593
21594 // Check if there's a newer version for download.
21595 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
21596
21597 $update = null;
21598 if ( is_object( $new_version ) ) {
21599 $update = new FS_Plugin_Tag( $new_version );
21600
21601 if ( ! $background ) {
21602 $this->_admin_notices->add(
21603 sprintf(
21604 /* translators: %s: Numeric version number (e.g. '2.1.9' */
21605 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
21606 $update->version,
21607 sprintf(
21608 '<a href="%s" target="_blank" rel="noopener">%s</a>',
21609 $this->get_account_url( 'download_latest' ),
21610 sprintf(
21611 /* translators: %s: plan name (e.g. latest "Professional" version) */
21612 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
21613 $this->get_plan_title()
21614 )
21615 )
21616 ),
21617 $this->get_text_inline( 'New', 'new' ) . '!'
21618 );
21619 }
21620 } else if ( false === $new_version && ! $background ) {
21621 $this->_admin_notices->add(
21622 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
21623 $this->get_text_inline( 'You are all good!', 'you-are-good' )
21624 );
21625 }
21626
21627 $this->_store_update( $update, true, $plugin_id );
21628 }
21629
21630 /**
21631 * @author Vova Feldman (@svovaf)
21632 * @since 1.0.4
21633 *
21634 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
21635 *
21636 * @return FS_Plugin[]
21637 *
21638 * @uses FS_Api
21639 */
21640 private function sync_addons( $flush = false ) {
21641 $this->_logger->entrance();
21642
21643 $api = $this->get_api_site_or_plugin_scope();
21644
21645 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
21646
21647 /**
21648 * @since 1.2.1
21649 *
21650 * If there's a cached version of the add-ons and not asking
21651 * for a flush, just use the currently stored add-ons.
21652 */
21653 if ( ! $flush && $api->is_cached( $path ) ) {
21654 $addons = self::get_all_addons();
21655
21656 return isset( $addons[ $this->_plugin->id ] ) ?
21657 $addons[ $this->_plugin->id ] :
21658 array();
21659 }
21660
21661 $result = $api->get( $path, $flush );
21662
21663 $addons = array();
21664 if ( $this->is_api_result_object( $result, 'plugins' ) &&
21665 is_array( $result->plugins )
21666 ) {
21667 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
21668 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
21669 }
21670
21671 $this->_store_addons( $addons, true );
21672 }
21673
21674 return $addons;
21675 }
21676
21677 /**
21678 * Handle user email update.
21679 *
21680 * @author Vova Feldman (@svovaf)
21681 * @since 1.0.3
21682 * @uses FS_Api
21683 *
21684 * @param string $new_email
21685 *
21686 * @return object
21687 */
21688 private function update_email( $new_email ) {
21689 $this->_logger->entrance();
21690
21691
21692 $api = $this->get_api_user_scope();
21693 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
21694 'email' => $new_email,
21695 'after_email_confirm_url' => $this->_get_admin_page_url(
21696 'account',
21697 array( 'fs_action' => 'sync_user' )
21698 ),
21699 ) );
21700
21701 if ( ! isset( $user->error ) ) {
21702 $this->_user->email = $user->email;
21703 $this->_user->is_verified = $user->is_verified;
21704 $this->_store_user();
21705 } else {
21706 // handle different error cases.
21707
21708 }
21709
21710 return $user;
21711 }
21712
21713 #----------------------------------------------------------------------------------
21714 #region API Error Handling
21715 #----------------------------------------------------------------------------------
21716
21717 /**
21718 * @author Vova Feldman (@svovaf)
21719 * @since 1.1.1
21720 *
21721 * @param mixed $result
21722 *
21723 * @return bool Is API result contains an error.
21724 */
21725 private function is_api_error( $result ) {
21726 return FS_Api::is_api_error( $result );
21727 }
21728
21729 /**
21730 * Checks if given API result is a non-empty and not an error object.
21731 *
21732 * @author Vova Feldman (@svovaf)
21733 * @since 1.2.1.5
21734 *
21735 * @param mixed $result
21736 * @param string|null $required_property Optional property we want to verify that is set.
21737 *
21738 * @return bool
21739 */
21740 function is_api_result_object( $result, $required_property = null ) {
21741 return FS_Api::is_api_result_object( $result, $required_property );
21742 }
21743
21744 /**
21745 * Checks if given API result is a non-empty entity object with non-empty ID.
21746 *
21747 * @author Vova Feldman (@svovaf)
21748 * @since 1.2.1.5
21749 *
21750 * @param mixed $result
21751 *
21752 * @return bool
21753 */
21754 private function is_api_result_entity( $result ) {
21755 return FS_Api::is_api_result_entity( $result );
21756 }
21757
21758 #endregion
21759
21760 /**
21761 * Make sure a given argument is an array of a specific type.
21762 *
21763 * @author Vova Feldman (@svovaf)
21764 * @since 1.2.1.5
21765 *
21766 * @param mixed $array
21767 * @param string $class
21768 *
21769 * @return bool
21770 */
21771 private function is_array_instanceof( $array, $class ) {
21772 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
21773 }
21774
21775 /**
21776 * Start install ownership change.
21777 *
21778 * @author Vova Feldman (@svovaf)
21779 * @since 1.1.1
21780 * @uses FS_Api
21781 *
21782 * @param string $candidate_email
21783 *
21784 * @return bool Is ownership change successfully initiated.
21785 */
21786 private function init_change_owner( $candidate_email ) {
21787 $this->_logger->entrance();
21788
21789 $api = $this->get_api_site_scope();
21790 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
21791 'email' => $candidate_email,
21792 'after_confirm_url' => $this->_get_admin_page_url(
21793 'account',
21794 array( 'fs_action' => 'change_owner' )
21795 ),
21796 ) );
21797
21798 return ! $this->is_api_error( $result );
21799 }
21800
21801 /**
21802 * Handle install ownership change.
21803 *
21804 * @author Vova Feldman (@svovaf)
21805 * @since 1.1.1
21806 * @uses FS_Api
21807 *
21808 * @return bool Was ownership change successfully complete.
21809 */
21810 private function complete_change_owner() {
21811 $this->_logger->entrance();
21812
21813 $site_result = $this->get_api_site_scope( true )->get();
21814 $site = new FS_Site( $site_result );
21815 $this->_site = $site;
21816
21817 $user = new FS_User();
21818 $user->id = fs_request_get( 'user_id' );
21819
21820 // Validate install's user and given user.
21821 if ( $user->id != $this->_site->user_id ) {
21822 return false;
21823 }
21824
21825 $user->public_key = fs_request_get( 'user_public_key' );
21826 $user->secret_key = fs_request_get( 'user_secret_key' );
21827
21828 // Fetch new user information.
21829 $this->_user = $user;
21830 $user_result = $this->get_api_user_scope( true )->get();
21831 $user = new FS_User( $user_result );
21832 $this->_user = $user;
21833
21834 $this->_set_account( $user, $site );
21835
21836 return true;
21837 }
21838
21839 /**
21840 * Completes ownership change by license.
21841 *
21842 * @author Leo Fajardo (@leorw)
21843 * @since 2.3.2
21844 *
21845 * @param number $user_id
21846 * @param array[string]number $install_ids_by_slug_map
21847 *
21848 */
21849 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
21850 $this->_logger->entrance();
21851
21852 $this->sync_user_by_current_install( $user_id );
21853
21854 $result = $this->get_api_user_scope( true )->get(
21855 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
21856 );
21857
21858 if ( $this->is_api_result_object( $result, 'installs' ) ) {
21859 $sites = self::get_all_sites( $this->get_module_type() );
21860 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
21861
21862 foreach ( $result->installs as $install ) {
21863 $site = new FS_Site( $install );
21864
21865 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
21866 }
21867
21868 $this->set_account_option( 'sites', $sites, true );
21869 }
21870 }
21871
21872 /**
21873 * Handle user name update.
21874 *
21875 * @author Vova Feldman (@svovaf)
21876 * @since 1.0.9
21877 * @uses FS_Api
21878 *
21879 * @return object
21880 */
21881 private function update_user_name() {
21882 $this->_logger->entrance();
21883 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
21884
21885 $api = $this->get_api_user_scope();
21886 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
21887 'name' => $name,
21888 ) );
21889
21890 if ( ! isset( $user->error ) ) {
21891 $this->_user->first = $user->first;
21892 $this->_user->last = $user->last;
21893 $this->_store_user();
21894 } else {
21895 // handle different error cases.
21896
21897 }
21898
21899 return $user;
21900 }
21901
21902 /**
21903 * Verify user email.
21904 *
21905 * @author Vova Feldman (@svovaf)
21906 * @since 1.0.3
21907 * @uses FS_Api
21908 */
21909 private function verify_email() {
21910 $this->_handle_account_user_sync();
21911
21912 if ( $this->_user->is_verified() ) {
21913 return;
21914 }
21915
21916 $api = $this->get_api_site_scope();
21917 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
21918 'after_email_confirm_url' => $this->_get_admin_page_url(
21919 'account',
21920 array( 'fs_action' => 'sync_user' )
21921 )
21922 ) );
21923
21924 if ( ! isset( $result->error ) ) {
21925 $this->_admin_notices->add( sprintf(
21926 $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' ),
21927 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
21928 ) );
21929 } else {
21930 // handle different error cases.
21931
21932 }
21933 }
21934
21935 /**
21936 * @author Vova Feldman (@svovaf)
21937 * @since 1.1.2
21938 *
21939 * @param array $params
21940 * @param bool|null $network
21941 *
21942 * @return string
21943 */
21944 function get_activation_url( $params = array(), $network = null ) {
21945 if ( $this->is_addon() && $this->has_free_plan() ) {
21946 /**
21947 * @author Vova Feldman (@svovaf)
21948 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
21949 */
21950 return $this->get_parent_instance()->get_activation_url( $params );
21951 }
21952
21953 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
21954 }
21955
21956 /**
21957 * @author Vova Feldman (@svovaf)
21958 * @since 1.2.1.5
21959 *
21960 * @param array $params
21961 *
21962 * @return string
21963 */
21964 function get_reconnect_url( $params = array() ) {
21965 $params['fs_action'] = 'reset_anonymous_mode';
21966 $params['fs_unique_affix'] = $this->get_unique_affix();
21967
21968 return $this->get_activation_url( $params );
21969 }
21970
21971 /**
21972 * Get the URL of the page that should be loaded after the user connect
21973 * or skip in the opt-in screen.
21974 *
21975 * @author Vova Feldman (@svovaf)
21976 * @since 1.1.3
21977 *
21978 * @param string $filter Filter name.
21979 * @param array $params Since 1.2.2.7
21980 * @param bool|null $network
21981 *
21982 * @return string
21983 */
21984 function get_after_activation_url( $filter, $params = array(), $network = null ) {
21985 if ( $this->show_opt_in_on_themes_page() &&
21986 ( fs_request_has( 'pending_activation' ) ||
21987 // For cases when the first time path is set, even though it's a WP.org theme.
21988 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
21989 ) {
21990 $first_time_path = '';
21991 } else {
21992 $first_time_path = $this->_menu->get_first_time_path(
21993 fs_is_network_admin() && $this->_is_network_active
21994 );
21995 }
21996
21997 if ( $this->_is_network_active &&
21998 fs_is_network_admin() &&
21999 ! $this->_menu->has_network_menu() &&
22000 $this->is_network_registered()
22001 ) {
22002 $target_url = $this->get_account_url();
22003 } else {
22004 // Default plugin's page.
22005 $target_url = $this->_get_admin_page_url( '', array(), $network );
22006 }
22007
22008 return add_query_arg( $params, $this->apply_filters(
22009 $filter,
22010 empty( $first_time_path ) ?
22011 $target_url :
22012 $first_time_path
22013 ) );
22014 }
22015
22016 /**
22017 * Handle account page updates / edits / actions.
22018 *
22019 * @author Vova Feldman (@svovaf)
22020 * @since 1.0.2
22021 *
22022 */
22023 private function _handle_account_edits() {
22024 if ( ! $this->is_user_admin() ) {
22025 return;
22026 }
22027
22028 $action = fs_get_action();
22029
22030 if ( empty( $action ) ) {
22031 return;
22032 }
22033
22034 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22035 $install_id = fs_request_get( 'install_id', '' );
22036
22037 // Alias.
22038 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22039
22040 $is_network_action = $this->is_network_level_action();
22041 $blog_id = $this->is_network_level_site_specific_action();
22042 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22043
22044 if ( is_numeric( $blog_id ) ) {
22045 $this->switch_to_blog( $blog_id );
22046 } else {
22047 $blog_id = '';
22048 }
22049
22050 switch ( $action ) {
22051 case 'opt_in':
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 $this->install_with_user(
22058 $this->get_network_user(),
22059 false,
22060 false,
22061 false,
22062 false
22063 );
22064
22065 $this->_admin_notices->add(
22066 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22067 $this->get_text_inline( 'Awesome', 'awesome' )
22068 );
22069 }
22070 }
22071 }
22072 break;
22073
22074 case 'toggle_tracking':
22075 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22076
22077 if ( $is_parent_plugin_action ) {
22078 if ( $is_network_action && ! empty( $blog_id ) ) {
22079 if ( $this->is_registered() ) {
22080 if ( $this->is_tracking_prohibited() ) {
22081 if ( $this->allow_site_tracking() ) {
22082 $this->_admin_notices->add(
22083 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 ),
22084 $this->get_text_inline( 'Thank you!', 'thank-you' )
22085 );
22086 }
22087 } else {
22088 if ( $this->stop_site_tracking() ) {
22089 $this->_admin_notices->add(
22090 sprintf(
22091 $this->get_text_inline( 'We will no longer be sending any usage data of %s on %s to %s.', 'opted-out-successfully' ),
22092 $this->get_plugin_title(),
22093 fs_strip_url_protocol( get_site_url( $blog_id ) ),
22094 sprintf(
22095 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22096 'https://freemius.com',
22097 'freemius.com'
22098 )
22099 )
22100 );
22101 }
22102 }
22103 }
22104 }
22105 }
22106
22107 break;
22108
22109 case 'delete_account':
22110 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22111
22112 $is_network_deletion = $is_network_action && empty( $blog_id );
22113
22114 if ( $is_parent_plugin_action ) {
22115 // Delete add-on installs if have any.
22116 $installed_addons = $this->get_installed_addons();
22117 foreach ( $installed_addons as $fs_addon ) {
22118 if ( $is_network_deletion ) {
22119 $fs_addon->delete_network_account_event();
22120 } else {
22121 $fs_addon->delete_account_event();
22122 }
22123 }
22124
22125 if ( $is_network_deletion ) {
22126 $this->delete_network_account_event();
22127 } else {
22128 $this->delete_account_event();
22129 }
22130
22131 // Clear user and site.
22132 $this->_site = null;
22133 $this->_user = null;
22134
22135 $this->maybe_set_slug_and_network_menu_exists_flag();
22136
22137 fs_redirect( $this->get_activation_url() );
22138 } else {
22139 if ( $this->is_addon_activated( $plugin_id ) ) {
22140 $fs_addon = self::get_instance_by_id( $plugin_id );
22141
22142 if ( $is_network_deletion ) {
22143 $fs_addon->delete_network_account_event();
22144 } else {
22145 $fs_addon->delete_account_event();
22146 }
22147
22148 fs_redirect( $this->_get_admin_page_url( 'account' ) );
22149 }
22150 }
22151
22152 return;
22153
22154 case 'downgrade_account':
22155 if ( is_numeric( $blog_id ) ) {
22156 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22157 } else {
22158 check_admin_referer( $action );
22159 }
22160
22161 $switch_to_network_install_blog_after_cancellation = (
22162 is_numeric( $blog_id ) &&
22163 $plugin_id == $this->get_id() &&
22164 ! $this->is_trial()
22165 );
22166
22167 $result = $this->cancel_subscription_or_trial( $plugin_id );
22168 if ( $this->is_api_error( $result ) ) {
22169 $this->_admin_notices->add(
22170 $result->error->message,
22171 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22172 'error'
22173 );
22174 }
22175
22176 if ( $switch_to_network_install_blog_after_cancellation ) {
22177 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22178 }
22179
22180 return;
22181
22182 case 'activate_license':
22183 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22184
22185 $fs = $this;
22186 if ( $plugin_id != $this->get_id() ) {
22187 $fs = $this->is_addon_activated( $plugin_id ) ?
22188 self::get_instance_by_id( $plugin_id ) :
22189 null;
22190 }
22191
22192 if ( is_object( $fs ) ) {
22193 $fs->_activate_license();
22194
22195 /**
22196 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
22197 *
22198 * @author Leo Fajardo (@leorw)
22199 * @since 2.4.0
22200 */
22201 unset( $_REQUEST['plugin_id'] );
22202
22203 if ( $this->is_bundle_license_auto_activation_enabled() ) {
22204 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
22205 }
22206 }
22207
22208 return;
22209
22210 case 'deactivate_license':
22211 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22212
22213 if ( $plugin_id == $this->get_id() ) {
22214 $this->_deactivate_license();
22215
22216 if ( $this->is_only_premium() ) {
22217 // Clear user and site.
22218 $this->_site = null;
22219 $this->_user = null;
22220
22221 if ( ! $is_network_action ) {
22222 fs_redirect( $this->get_activation_url() );
22223 } else if ( is_numeric( $blog_id ) ) {
22224 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22225 }
22226 }
22227 } else {
22228 if ( $this->is_addon_activated( $plugin_id ) ) {
22229 $fs_addon = self::get_instance_by_id( $plugin_id );
22230 $fs_addon->_deactivate_license();
22231 }
22232 }
22233
22234 return;
22235
22236 case 'check_updates':
22237 check_admin_referer( $action );
22238 $this->check_updates();
22239
22240 return;
22241
22242 case 'change_owner':
22243 $state = fs_request_get( 'state', 'init' );
22244 switch ( $state ) {
22245 case 'init':
22246 $candidate_email = fs_request_get( 'candidate_email', '' );
22247
22248 if ( $this->init_change_owner( $candidate_email ) ) {
22249 $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>' ) );
22250 }
22251 break;
22252 case 'owner_confirmed':
22253 $candidate_email = fs_request_get( 'candidate_email', '' );
22254
22255 $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>' ) );
22256 break;
22257 case 'candidate_confirmed':
22258 if ( $this->complete_change_owner() ) {
22259 $this->_admin_notices->add_sticky(
22260 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
22261 'ownership_changed',
22262 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
22263 );
22264 } else {
22265 // @todo Handle failed ownership change message.
22266 }
22267 break;
22268 }
22269
22270 return;
22271
22272 case 'update_email':
22273 check_admin_referer( 'update_email' );
22274
22275 $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' );
22276 $result = $this->update_email( $new_email );
22277
22278 if ( isset( $result->error ) ) {
22279 switch ( $result->error->code ) {
22280 case 'user_exist':
22281 $this->_admin_notices->add(
22282 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
22283 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>' ) .
22284 sprintf(
22285 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
22286 $this->get_account_url( 'change_owner', array(
22287 'state' => 'init',
22288 'candidate_email' => $new_email
22289 ) ),
22290 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
22291 ),
22292 $oops_text,
22293 'error'
22294 );
22295 break;
22296 }
22297 } else {
22298 $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' ) );
22299 }
22300
22301 return;
22302
22303 case 'update_user_name':
22304 check_admin_referer( 'update_user_name' );
22305
22306 $result = $this->update_user_name();
22307
22308 if ( isset( $result->error ) ) {
22309 $this->_admin_notices->add(
22310 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
22311 $oops_text,
22312 'error'
22313 );
22314 } else {
22315 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
22316 }
22317
22318 return;
22319
22320 #region Actions that might be called from external links (e.g. email)
22321
22322 case 'cancel_trial':
22323 $result = $this->cancel_subscription_or_trial( $plugin_id );
22324 if ( $this->is_api_error( $result ) ) {
22325 $this->_admin_notices->add(
22326 $result->error->message,
22327 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22328 'error'
22329 );
22330 }
22331
22332 return;
22333
22334 case 'verify_email':
22335 $this->verify_email();
22336
22337 return;
22338
22339 case 'sync_user':
22340 $this->_handle_account_user_sync();
22341
22342 return;
22343
22344 case $this->get_unique_affix() . '_sync_license':
22345 $this->_sync_license();
22346
22347 return;
22348
22349 case 'download_latest':
22350 $this->download_latest_directly( $plugin_id );
22351
22352 return;
22353
22354 #endregion
22355 }
22356
22357 if ( WP_FS__IS_POST_REQUEST ) {
22358 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
22359 foreach ( $properties as $p ) {
22360 if ( 'update_' . $p === $action ) {
22361 check_admin_referer( $action );
22362
22363 $this->_logger->log( $action );
22364
22365 $site_property = substr( $p, strlen( 'site_' ) );
22366 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
22367 $this->get_site()->{$site_property} = $site_property_value;
22368
22369 // Store account after modification.
22370 $this->_store_site();
22371
22372 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
22373
22374 $this->_admin_notices->add( sprintf(
22375 /* translators: %s: User's account property (e.g. email address, name) */
22376 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
22377 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
22378 ) );
22379
22380 return;
22381 }
22382 }
22383 }
22384 }
22385
22386 /**
22387 * Account page resources load.
22388 *
22389 * @author Vova Feldman (@svovaf)
22390 * @since 1.0.6
22391 */
22392 function _account_page_load() {
22393 $this->_logger->entrance();
22394
22395 $this->_logger->info( var_export( $_REQUEST, true ) );
22396
22397 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
22398
22399 if ( $this->has_addons() ) {
22400 wp_enqueue_script( 'plugin-install' );
22401 add_thickbox();
22402
22403 function fs_addons_body_class( $classes ) {
22404 $classes .= ' plugins-php';
22405
22406 return $classes;
22407 }
22408
22409 add_filter( 'admin_body_class', 'fs_addons_body_class' );
22410 }
22411
22412 if ( $this->has_paid_plan() &&
22413 ! $this->has_any_license() &&
22414 ! $this->is_sync_executed() &&
22415 $this->is_tracking_allowed()
22416 ) {
22417 /**
22418 * If no licenses found and no sync job was executed during the last 24 hours,
22419 * just execute the sync job right away (blocking execution).
22420 *
22421 * @since 1.1.7.3
22422 */
22423 $this->run_manual_sync();
22424 }
22425
22426 $this->_handle_account_edits();
22427
22428 if (
22429 is_object( $this->_license ) &&
22430 $this->_license->user_id == $this->_user->id &&
22431 ! $this->is_whitelabeled( true )
22432 ) {
22433 $this->_admin_notices->add(
22434 sprintf(
22435 $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' ),
22436 sprintf(
22437 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
22438 $this->get_text_inline( 'Click here', 'click-here' )
22439 )
22440 ),
22441 '',
22442 'success',
22443 false,
22444 'license_not_whitelabeled'
22445 );
22446 }
22447
22448 $this->do_action( 'account_page_load_before_departure' );
22449 }
22450
22451 /**
22452 * Renders the "Affiliation" page.
22453 *
22454 * @author Leo Fajardo (@leorw)
22455 * @since 1.2.3
22456 */
22457 function _affiliation_page_render() {
22458 $this->_logger->entrance();
22459
22460 $this->fetch_affiliate_and_terms();
22461
22462 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
22463
22464 $vars = array( 'id' => $this->_module_id );
22465 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
22466 }
22467
22468
22469 /**
22470 * Render account page.
22471 *
22472 * @author Vova Feldman (@svovaf)
22473 * @since 1.0.0
22474 */
22475 function _account_page_render() {
22476 $this->_logger->entrance();
22477
22478 $template = 'account.php';
22479 $vars = array( 'id' => $this->_module_id );
22480
22481 /**
22482 * Added filter to the template to allow developers wrapping the template
22483 * in custom HTML (e.g. within a wizard/tabs).
22484 *
22485 * @author Vova Feldman (@svovaf)
22486 * @since 1.2.1.6
22487 */
22488 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
22489 }
22490
22491 /**
22492 * Render account connect page.
22493 *
22494 * @author Vova Feldman (@svovaf)
22495 * @since 1.0.7
22496 */
22497 function _connect_page_render() {
22498 $this->_logger->entrance();
22499
22500 $vars = array( 'id' => $this->_module_id );
22501
22502 /**
22503 * Added filter to the template to allow developers wrapping the template
22504 * in custom HTML (e.g. within a wizard/tabs).
22505 *
22506 * @author Vova Feldman (@svovaf)
22507 * @since 1.2.1.6
22508 */
22509 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
22510 }
22511
22512 /**
22513 * Load required resources before add-ons page render.
22514 *
22515 * @author Vova Feldman (@svovaf)
22516 * @since 1.0.6
22517 */
22518 function _addons_page_load() {
22519 $this->_logger->entrance();
22520
22521 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
22522
22523 wp_enqueue_script( 'plugin-install' );
22524 add_thickbox();
22525
22526 function fs_addons_body_class( $classes ) {
22527 $classes .= ' plugins-php';
22528
22529 return $classes;
22530 }
22531
22532 add_filter( 'admin_body_class', 'fs_addons_body_class' );
22533
22534 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
22535 $this->_admin_notices->add(
22536 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>' ),
22537 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
22538 'update-nag'
22539 );
22540 }
22541 }
22542
22543 /**
22544 * Render add-ons page.
22545 *
22546 * @author Vova Feldman (@svovaf)
22547 * @since 1.0.6
22548 */
22549 function _addons_page_render() {
22550 $this->_logger->entrance();
22551
22552 $vars = array( 'id' => $this->_module_id );
22553
22554 /**
22555 * Added filter to the template to allow developers wrapping the template
22556 * in custom HTML (e.g. within a wizard/tabs).
22557 *
22558 * @author Vova Feldman (@svovaf)
22559 * @since 1.2.1.6
22560 */
22561 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
22562 }
22563
22564 /* Pricing & Upgrade
22565 ------------------------------------------------------------------------------------------------------------------*/
22566 /**
22567 * Render pricing page.
22568 *
22569 * @author Vova Feldman (@svovaf)
22570 * @since 1.0.0
22571 */
22572 function _pricing_page_render() {
22573 $this->_logger->entrance();
22574
22575 $vars = array( 'id' => $this->_module_id );
22576
22577 if ( 'true' === fs_request_get( 'checkout', false ) ) {
22578 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
22579 } else {
22580 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
22581 }
22582 }
22583
22584 /**
22585 * @author Leo Fajardo (@leorw)
22586 * @since 2.3.1
22587 */
22588 function _maybe_add_pricing_ajax_handler() {
22589 if ( ! $this->should_use_external_pricing() ) {
22590 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
22591 }
22592 }
22593
22594 /**
22595 * @author Leo Fajardo (@leorw)
22596 * @since 2.3.1
22597 */
22598 function _fs_pricing_ajax_action_handler() {
22599 $this->check_ajax_referer( 'pricing_ajax_action' );
22600
22601 $result = null;
22602 $pricing_action = fs_request_get( 'pricing_action' );
22603
22604 switch ( $pricing_action ) {
22605 case 'fetch_pricing_data':
22606 $params = array(
22607 'is_enriched' => true,
22608 'trial' => fs_request_get_bool( 'trial' ),
22609 'sandbox' => fs_request_get( 'sandbox' ),
22610 's_ctx_type' => fs_request_get( 's_ctx_type' ),
22611 's_ctx_id' => fs_request_get( 's_ctx_id' ),
22612 's_ctx_ts' => fs_request_get( 's_ctx_ts' ),
22613 's_ctx_secure' => fs_request_get( 's_ctx_secure' ),
22614 );
22615
22616 $bundle_id = $this->get_bundle_id();
22617 $bundle_public_key = $this->get_bundle_public_key();
22618
22619 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
22620
22621 if ( ! $has_bundle_context ) {
22622 $api = $this->get_api_plugin_scope();
22623 } else {
22624 $api = FS_Api::instance(
22625 $bundle_id,
22626 'plugin',
22627 $bundle_id,
22628 $bundle_public_key,
22629 ! $this->is_live(),
22630 false,
22631 $this->get_sdk_version()
22632 );
22633
22634 $params['plugin_id'] = $this->get_id();
22635 $params['plugin_public_key'] = $this->get_public_key();
22636 }
22637
22638 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
22639 break;
22640 case 'start_trial':
22641 $result = $this->opt_in(
22642 false,
22643 false,
22644 false,
22645 false,
22646 false,
22647 fs_request_get( 'plan_id' )
22648 );
22649 }
22650
22651 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
22652 $this->_logger->api_error( $result );
22653
22654 self::shoot_ajax_failure(
22655 isset( $result->error ) ?
22656 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
22657 var_export( $result, true )
22658 );
22659 }
22660
22661 $this->shoot_ajax_success( $result );
22662 }
22663
22664 #----------------------------------------------------------------------------------
22665 #region Contact Us
22666 #----------------------------------------------------------------------------------
22667
22668 /**
22669 * Render contact-us page.
22670 *
22671 * @author Vova Feldman (@svovaf)
22672 * @since 1.0.3
22673 */
22674 function _contact_page_render() {
22675 $this->_logger->entrance();
22676
22677 $vars = array( 'id' => $this->_module_id );
22678
22679 /**
22680 * Added filter to the template to allow developers wrapping the template
22681 * in custom HTML (e.g. within a wizard/tabs).
22682 *
22683 * @author Vova Feldman (@svovaf)
22684 * @since 2.1.3
22685 */
22686 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
22687 }
22688
22689 #endregion ------------------------------------------------------------------------
22690
22691 /**
22692 * Hide all admin notices to prevent distractions.
22693 *
22694 * @author Vova Feldman (@svovaf)
22695 * @since 1.0.3
22696 *
22697 * @uses remove_all_actions()
22698 */
22699 private static function _hide_admin_notices() {
22700 remove_all_actions( 'admin_notices' );
22701 remove_all_actions( 'network_admin_notices' );
22702 remove_all_actions( 'all_admin_notices' );
22703 remove_all_actions( 'user_admin_notices' );
22704 }
22705
22706 static function _clean_admin_content_section_hook() {
22707 self::_hide_admin_notices();
22708
22709 // Hide footer.
22710 echo '<style>#wpfooter { display: none !important; }</style>';
22711 }
22712
22713 /**
22714 * Attach to admin_head hook to hide all admin notices.
22715 *
22716 * @author Vova Feldman (@svovaf)
22717 * @since 1.0.3
22718 */
22719 static function _clean_admin_content_section() {
22720 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
22721 }
22722
22723 /* CSS & JavaScript
22724 ------------------------------------------------------------------------------------------------------------------*/
22725 /* function _enqueue_script($handle, $src) {
22726 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
22727
22728 $this->_logger->entrance( 'script = ' . $url );
22729
22730 wp_enqueue_script( $handle, $url );
22731 }*/
22732
22733 /* SDK
22734 ------------------------------------------------------------------------------------------------------------------*/
22735 private $_user_api;
22736
22737 /**
22738 *
22739 * @author Vova Feldman (@svovaf)
22740 * @since 1.0.2
22741 *
22742 * @param bool $flush
22743 *
22744 * @return FS_Api
22745 */
22746 private function get_api_user_scope( $flush = false ) {
22747 if ( ! isset( $this->_user_api ) || $flush ) {
22748 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
22749 }
22750
22751 return $this->_user_api;
22752 }
22753
22754 /**
22755 * @author Vova Feldman (@svovaf)
22756 * @since 2.0.0
22757 *
22758 * @param \FS_User $user
22759 *
22760 * @return \FS_Api
22761 */
22762 private function get_api_user_scope_by_user( FS_User $user ) {
22763 return FS_Api::instance(
22764 $this->_module_id,
22765 'user',
22766 $user->id,
22767 $user->public_key,
22768 ! $this->is_live(),
22769 $user->secret_key,
22770 $this->get_sdk_version()
22771 );
22772 }
22773
22774 /**
22775 *
22776 * @author Leo Fajardo (@leorw)
22777 * @since 2.0.0
22778 *
22779 * @param bool $flush
22780 *
22781 * @return FS_Api
22782 */
22783 private function get_current_or_network_user_api_scope( $flush = false ) {
22784 if ( ! $this->_is_network_active ||
22785 ( isset( $this->_user ) && $this->_user instanceof FS_User )
22786 ) {
22787 return $this->get_api_user_scope( $flush );
22788 }
22789
22790 $user = $this->get_current_or_network_user();
22791
22792 $this->_user_api = FS_Api::instance(
22793 $this->_module_id,
22794 'user',
22795 $user->id,
22796 $user->public_key,
22797 ! $this->is_live(),
22798 $user->secret_key,
22799 $this->get_sdk_version()
22800 );
22801
22802 return $this->_user_api;
22803 }
22804
22805 private $_site_api;
22806
22807 /**
22808 *
22809 * @author Vova Feldman (@svovaf)
22810 * @since 1.0.2
22811 *
22812 * @param bool $flush
22813 *
22814 * @return FS_Api
22815 */
22816 private function get_api_site_scope( $flush = false ) {
22817 if ( ! isset( $this->_site_api ) || $flush ) {
22818 $this->_site_api = FS_Api::instance(
22819 $this->_module_id,
22820 'install',
22821 $this->_site->id,
22822 $this->_site->public_key,
22823 ! $this->is_live(),
22824 $this->_site->secret_key,
22825 $this->get_sdk_version()
22826 );
22827 }
22828
22829 return $this->_site_api;
22830 }
22831
22832 private $_plugin_api;
22833
22834 /**
22835 * Get plugin public API scope.
22836 *
22837 * @author Vova Feldman (@svovaf)
22838 * @since 1.0.7
22839 *
22840 * @return FS_Api
22841 */
22842 function get_api_plugin_scope() {
22843 if ( ! isset( $this->_plugin_api ) ) {
22844 $this->_plugin_api = FS_Api::instance(
22845 $this->_module_id,
22846 'plugin',
22847 $this->_plugin->id,
22848 $this->_plugin->public_key,
22849 ! $this->is_live(),
22850 false,
22851 $this->get_sdk_version()
22852 );
22853 }
22854
22855 return $this->_plugin_api;
22856 }
22857
22858 /**
22859 * Get bundle public API scope.
22860 *
22861 * @author Vova Feldman (@svovaf)
22862 * @since 2.3.1
22863 *
22864 * @return FS_Api
22865 */
22866 function get_api_bundle_scope() {
22867 return FS_Api::instance(
22868 $this->get_bundle_id(),
22869 'plugin',
22870 $this->get_bundle_id(),
22871 $this->get_bundle_public_key(),
22872 ! $this->is_live(),
22873 false,
22874 $this->get_sdk_version()
22875 );
22876 }
22877
22878 /**
22879 * Get site API scope object (fallback to public plugin scope when not registered).
22880 *
22881 * @author Vova Feldman (@svovaf)
22882 * @since 1.0.7
22883 *
22884 * @return FS_Api
22885 */
22886 function get_api_site_or_plugin_scope() {
22887 return $this->is_registered() ?
22888 $this->get_api_site_scope() :
22889 $this->get_api_plugin_scope();
22890 }
22891
22892 /**
22893 * @author Leo Fajardo (@leorw)
22894 * @since 2.2.3.1
22895 *
22896 * @param object $result
22897 */
22898 private function maybe_modify_api_curl_error_message( $result ) {
22899 if (
22900 'cUrlMissing' !== $result->error->type &&
22901 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
22902 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
22903 ) {
22904 return;
22905 }
22906
22907 $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' ) .
22908 ' ' .
22909 $this->esc_html_inline(
22910 sprintf(
22911 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
22912 implode(
22913 ', ',
22914 $this->apply_filters( 'api_domains', array(
22915 'api.freemius.com',
22916 'wp.freemius.com'
22917 ) )
22918 )
22919 ),
22920 'connectivity-whitelist'
22921 ) .
22922 ' ' .
22923 sprintf(
22924 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
22925 $this->get_module_type()
22926 );
22927 }
22928
22929 /**
22930 * Show trial promotional notice (if any trial exist).
22931 *
22932 * @author Vova Feldman (@svovaf)
22933 * @since 1.0.9
22934 *
22935 * @param FS_Plugin_Plan[] $plans
22936 */
22937 function _check_for_trial_plans( $plans ) {
22938 /**
22939 * 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.
22940 *
22941 * @author Vova Feldman (@svovaf)
22942 * @since 2.1.2
22943 */
22944 if ( ! is_array( $plans ) && is_object( $plans ) ) {
22945 $plans = array( $plans );
22946 }
22947
22948 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
22949 $plans = array();
22950 }
22951
22952 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
22953 }
22954
22955 /**
22956 * During trial promotion the "upgrade" submenu item turns to
22957 * "start trial" to encourage the trial. Since we want to keep
22958 * the same menu item handler and there's no robust way to
22959 * add new arguments to the menu item link's querystring,
22960 * use JavaScript to find the menu item and update the href of
22961 * the link.
22962 *
22963 * @author Vova Feldman (@svovaf)
22964 * @since 1.2.1.5
22965 */
22966 function _fix_start_trial_menu_item_url() {
22967 $template_args = array( 'id' => $this->_module_id );
22968 fs_require_template( 'add-trial-to-pricing.php', $template_args );
22969 }
22970
22971 /**
22972 * Check if module is currently in a trial promotion mode.
22973 *
22974 * @author Vova Feldman (@svovaf)
22975 * @since 1.2.2.7
22976 *
22977 * @return bool
22978 */
22979 function is_in_trial_promotion() {
22980 return $this->_admin_notices->has_sticky( 'trial_promotion' );
22981 }
22982
22983 /**
22984 * Show trial promotional notice (if any trial exist).
22985 *
22986 * @author Vova Feldman (@svovaf)
22987 * @since 1.0.9
22988 *
22989 * @return bool If trial notice added.
22990 */
22991 function _add_trial_notice() {
22992 if ( ! $this->is_user_admin() ) {
22993 return false;
22994 }
22995
22996 if ( ! $this->is_user_in_admin() ) {
22997 return false;
22998 }
22999
23000 if ( $this->_is_network_active ) {
23001 if ( fs_is_network_admin() ) {
23002 // Network level trial is disabled at the moment.
23003 return false;
23004 }
23005
23006 if ( ! $this->is_delegated_connection() ) {
23007 // Only delegated sites should support trials.
23008 return false;
23009 }
23010 }
23011
23012 // Check if trial message is already shown.
23013 if ( $this->is_in_trial_promotion() ) {
23014 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23015
23016 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23017
23018 return false;
23019 }
23020
23021 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
23022 // Don't show trial if running the premium code, unless running in DEV mode.
23023 return false;
23024 }
23025
23026 if ( ! $this->has_trial_plan() ) {
23027 // No plans with trial.
23028 return false;
23029 }
23030
23031 if ( ! $this->apply_filters( 'show_trial', true ) ) {
23032 // Developer explicitly asked not to show the trial promo.
23033 return false;
23034 }
23035
23036 if ( $this->is_registered() ) {
23037 // Check if trial already utilized.
23038 if ( $this->_site->is_trial_utilized() ) {
23039 return false;
23040 }
23041
23042 if ( $this->is_paying_or_trial() ) {
23043 // Don't show trial if paying or already in trial.
23044 return false;
23045 }
23046 }
23047
23048 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23049 // If not yet opted-in/skipped, or pending activation, don't show trial.
23050 return false;
23051 }
23052
23053 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
23054 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
23055
23056 // Show promotion if never shown before and 24 hours after initial activation with FS.
23057 if ( ! $was_promotion_shown_before &&
23058 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
23059 ) {
23060 return false;
23061 }
23062
23063 // OR if promotion was shown before, try showing it every 30 days.
23064 if ( $was_promotion_shown_before &&
23065 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
23066 ) {
23067 return false;
23068 }
23069
23070 $trial_period = $this->_trial_days;
23071 $require_payment = $this->_is_trial_require_payment;
23072 $trial_url = $this->get_trial_url();
23073 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
23074
23075 if ( $this->is_registered() ) {
23076 // If opted-in, override trial with up to date data from API.
23077 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
23078 $trial_plans_count = count( $trial_plans );
23079
23080 if ( 0 === $trial_plans_count ) {
23081 // If there's no plans with a trial just exit.
23082 return false;
23083 }
23084
23085 /**
23086 * @var FS_Plugin_Plan $paid_plan
23087 */
23088 $paid_plan = $trial_plans[0];
23089 $require_payment = $paid_plan->is_require_subscription;
23090 $trial_period = $paid_plan->trial_period;
23091
23092 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
23093
23094 if ( $total_paid_plans !== $trial_plans_count ) {
23095 // Not all paid plans have a trial - generate a string of those that have it.
23096 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
23097 $plans_string .= sprintf(
23098 ' <a href="%s">%s</a>',
23099 $trial_url,
23100 $trial_plans[ $i ]->title
23101 );
23102
23103 if ( $i < $trial_plans_count - 2 ) {
23104 $plans_string .= ', ';
23105 } else if ( $i == $trial_plans_count - 2 ) {
23106 $plans_string .= ' and ';
23107 }
23108 }
23109 }
23110 }
23111
23112 $message = sprintf(
23113 $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' ),
23114 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
23115 $plans_string,
23116 $trial_period
23117 );
23118
23119 // "No Credit-Card Required" or "No Commitment for N Days".
23120 $cc_string = $require_payment ?
23121 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
23122 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
23123
23124
23125 // Start trial button.
23126 $button = ' ' . sprintf(
23127 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
23128 $trial_url,
23129 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
23130 );
23131
23132 $this->_admin_notices->add_sticky(
23133 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
23134 'trial_promotion',
23135 '',
23136 'promotion'
23137 );
23138
23139 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
23140
23141 return true;
23142 }
23143
23144 /**
23145 * Lets users/customers know that the product has an affiliate program.
23146 *
23147 * @author Leo Fajardo (@leorw)
23148 * @since 1.2.2.11
23149 *
23150 * @return bool Returns true if the notice has been added.
23151 */
23152 function _add_affiliate_program_notice() {
23153 if ( ! $this->is_user_admin() ) {
23154 return false;
23155 }
23156
23157 if ( ! $this->is_user_in_admin() ) {
23158 return false;
23159 }
23160
23161 // Check if the notice is already shown.
23162 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
23163 return false;
23164 }
23165
23166 if (
23167 // Product has no affiliate program.
23168 ! $this->has_affiliate_program() ||
23169 // User has applied for an affiliate account.
23170 ! empty( $this->_storage->affiliate_application_data )
23171 ) {
23172 return false;
23173 }
23174
23175 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
23176 // Developer explicitly asked not to show the notice about the affiliate program.
23177 return false;
23178 }
23179
23180 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23181 // If not yet opted in/skipped, or pending activation, don't show the notice.
23182 return false;
23183 }
23184
23185 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
23186 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
23187
23188 /**
23189 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
23190 * activation with FS.
23191 */
23192 if ( $was_notice_shown_before ||
23193 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
23194 ) {
23195 return false;
23196 }
23197
23198 if ( ! $this->is_paying() &&
23199 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
23200 ) {
23201 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
23202 return false;
23203 }
23204
23205 $message = sprintf(
23206 $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' ),
23207 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
23208 $this->get_module_label( true )
23209 );
23210
23211 // HTML code for the "Learn more..." button.
23212 $button = ' ' . sprintf(
23213 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
23214 $this->_get_admin_page_url( 'affiliation' ),
23215 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
23216 );
23217
23218 $this->_admin_notices->add_sticky(
23219 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
23220 'affiliate_program',
23221 '',
23222 'promotion'
23223 );
23224
23225 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
23226
23227 return true;
23228 }
23229
23230 /**
23231 * @author Vova Feldman (@svovaf)
23232 * @since 1.2.1.5
23233 */
23234 function _enqueue_common_css() {
23235 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
23236 // Add basic CSS for admin-notices and menu-item colors.
23237 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23238 }
23239 }
23240
23241 /**
23242 * @author Leo Fajardo (@leorw)
23243 * @since 1.2.2
23244 */
23245 function _show_theme_activation_optin_dialog() {
23246 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
23247
23248 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
23249 }
23250
23251 /**
23252 * @author Leo Fajardo (@leorw)
23253 * @since 1.2.2
23254 */
23255 function _add_fs_theme_activation_dialog() {
23256 global $pagenow;
23257
23258 if ( 'themes.php' !== $pagenow ) {
23259 return;
23260 }
23261
23262 $vars = array( 'id' => $this->_module_id );
23263 fs_require_once_template( 'connect.php', $vars );
23264 }
23265
23266 /* Action Links
23267 ------------------------------------------------------------------------------------------------------------------*/
23268 private $_action_links_hooked = false;
23269 private $_action_links = array();
23270
23271 /**
23272 * Hook to plugin action links filter.
23273 *
23274 * @author Vova Feldman (@svovaf)
23275 * @since 1.0.0
23276 */
23277 private function hook_plugin_action_links() {
23278 $this->_logger->entrance();
23279
23280 $this->_action_links_hooked = true;
23281
23282 $this->_logger->log( 'Adding action links hooks.' );
23283
23284 // Add action link to settings page.
23285 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
23286 &$this,
23287 '_modify_plugin_action_links_hook'
23288 ), WP_FS__DEFAULT_PRIORITY, 2 );
23289 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
23290 &$this,
23291 '_modify_plugin_action_links_hook'
23292 ), WP_FS__DEFAULT_PRIORITY, 2 );
23293 }
23294
23295 /**
23296 * Add plugin action link.
23297 *
23298 * @author Vova Feldman (@svovaf)
23299 * @since 1.0.0
23300 *
23301 * @param $label
23302 * @param $url
23303 * @param bool $external
23304 * @param int $priority
23305 * @param bool $key
23306 */
23307 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
23308 $this->_logger->entrance();
23309
23310 if ( ! isset( $this->_action_links[ $priority ] ) ) {
23311 $this->_action_links[ $priority ] = array();
23312 }
23313
23314 if ( false === $key ) {
23315 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
23316 }
23317
23318 $this->_action_links[ $priority ][] = array(
23319 'label' => $label,
23320 'href' => $url,
23321 'key' => $key,
23322 'external' => $external
23323 );
23324 }
23325
23326 /**
23327 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
23328 *
23329 * @author Vova Feldman (@svovaf)
23330 * @since 1.0.0
23331 */
23332 function _add_upgrade_action_link() {
23333 $this->_logger->entrance();
23334
23335 $is_activation_mode = $this->is_activation_mode();
23336
23337 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
23338
23339 /**
23340 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
23341 * and "Add-Ons" menus should be added.
23342 *
23343 * @author Leo Fajardo (@leorw)
23344 * @since 2.3.0
23345 */
23346 $add_upgrade_link = (
23347 $add_action_links ||
23348 ( $is_activation_mode && $this->is_only_premium() )
23349 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
23350
23351 $add_addons_link = ( $add_action_links && $this->has_addons() );
23352
23353 if ( ! $add_upgrade_link && ! $add_addons_link ) {
23354 return;
23355 }
23356
23357 if (
23358 $add_upgrade_link &&
23359 $this->is_pricing_page_visible() &&
23360 $this->is_submenu_item_visible( 'pricing' )
23361 ) {
23362 $this->add_plugin_action_link(
23363 $this->get_text_inline( 'Upgrade', 'upgrade' ),
23364 $this->get_upgrade_url(),
23365 false,
23366 7,
23367 'upgrade'
23368 );
23369 }
23370
23371 if (
23372 $add_addons_link &&
23373 $this->has_addons() &&
23374 $this->is_submenu_item_visible( 'addons' )
23375 ) {
23376 $this->add_plugin_action_link(
23377 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
23378 $this->_get_admin_page_url( 'addons' ),
23379 false,
23380 9,
23381 'addons'
23382 );
23383 }
23384 }
23385
23386 /**
23387 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
23388 *
23389 * @author Leo Fajardo (@leorw)
23390 * @since 1.1.9
23391 */
23392 function _add_license_action_link() {
23393 $this->_logger->entrance();
23394
23395 if ( ! self::is_ajax() ) {
23396 // Inject license activation dialog UI and client side code.
23397 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
23398 }
23399
23400 $link_text = $this->is_free_plan() ?
23401 $this->get_text_inline( 'Activate License', 'activate-license' ) :
23402 $this->get_text_inline( 'Change License', 'change-license' );
23403
23404 $this->add_plugin_action_link(
23405 $link_text,
23406 '#',
23407 false,
23408 11,
23409 ( 'activate-license ' . $this->get_unique_affix() )
23410 );
23411 }
23412
23413 /**
23414 * @author Leo Fajardo (@leorw)
23415 * @since 2.0.2
23416 */
23417 function _add_premium_version_upgrade_selection_action() {
23418 $this->_logger->entrance();
23419
23420 if ( ! self::is_ajax() ) {
23421 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
23422 }
23423 }
23424
23425 /**
23426 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
23427 *
23428 * @author Leo Fajardo (@leorw)
23429 * @since 1.2.1.5
23430 */
23431 function _add_tracking_links() {
23432 if ( ! current_user_can( 'manage_options' ) ) {
23433 return;
23434 }
23435
23436 $this->_logger->entrance();
23437
23438 /**
23439 * @author Vova Feldman (@svovaf)
23440 * @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.
23441 */
23442 /*if ( $this->is_premium() ) {
23443 // Don't add opt-in/out for premium code base.
23444 return;
23445 }*/
23446
23447 if ( $this->is_only_premium() && $this->is_free_plan() ) {
23448 // 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.
23449 return;
23450 }
23451
23452 if (
23453 $this->is_addon() &&
23454 ! $this->is_only_premium() &&
23455 $this->_parent->is_anonymous()
23456 ) {
23457 return;
23458 }
23459
23460 if ( fs_is_network_admin() ) {
23461 if ( ! $this->_is_network_active ) {
23462 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
23463 return;
23464 } else if ( $this->is_network_delegated_connection() ) {
23465 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
23466 return;
23467 }
23468 } else {
23469 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
23470 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
23471 return;
23472 }
23473 }
23474
23475 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
23476 if ( ! $this->is_registered() && $this->is_anonymous() ) {
23477 $this->connect_again();
23478
23479 return;
23480 }
23481 }
23482
23483 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
23484 ( $this->is_theme() && ! self::is_themes_page() )
23485 ) {
23486 // Only show tracking links on the plugins and themes pages.
23487 return;
23488 }
23489
23490 if (
23491 $this->is_activation_mode() &&
23492 $this->is_premium() &&
23493 ! $this->is_registered()
23494 ) {
23495 // If not yet registered and running the premium code base, a license activation link will already be shown.
23496 return;
23497 }
23498
23499 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23500 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
23501 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
23502 return;
23503 }
23504 }
23505
23506 if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) {
23507 return;
23508 }
23509
23510 if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) {
23511 return;
23512 }
23513
23514 if ( $this->add_ajax_action( 'update_tracking_permission', array( &$this, '_update_tracking_permission_callback' ) ) ) {
23515 return;
23516 }
23517
23518 $link_text_id = '';
23519 $url = '#';
23520
23521 if ( $this->is_registered() ) {
23522 if ( $this->is_tracking_allowed() ) {
23523 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
23524 } else {
23525 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
23526 }
23527 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
23528 /**
23529 * Show opt-in link only if skipped or in activation mode.
23530 */
23531 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
23532
23533 $params = ! $this->is_anonymous() ?
23534 array() :
23535 array(
23536 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
23537 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
23538 );
23539
23540 $url = $this->get_activation_url( $params );
23541 }
23542
23543 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
23544
23545 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
23546 $this->add_plugin_action_link(
23547 $link_text_id,
23548 $url,
23549 false,
23550 13,
23551 "opt-in-or-opt-out {$this->_slug}"
23552 );
23553 }
23554 }
23555
23556 /**
23557 * Get the URL of the page that should be loaded right after the plugin activation.
23558 *
23559 * @author Vova Feldman (@svovaf)
23560 * @since 1.1.7.4
23561 *
23562 * @return string
23563 */
23564 function get_after_plugin_activation_redirect_url() {
23565 $url = false;
23566
23567 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
23568 $first_time_path = $this->_menu->get_first_time_path(
23569 fs_is_network_admin() && $this->_is_network_active
23570 );
23571
23572 if ( $this->is_activation_mode() ) {
23573 $url = $this->get_activation_url();
23574 } else if ( ! empty( $first_time_path ) ) {
23575 $url = $first_time_path;
23576 } else {
23577 $page = '';
23578 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
23579 if ( $this->is_network_registered() ) {
23580 $page = 'account';
23581 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
23582 $this->maybe_set_slug_and_network_menu_exists_flag();
23583 }
23584 }
23585
23586 $url = $this->_get_admin_page_url( $page );
23587 }
23588 } else {
23589 $plugin_fs = false;
23590
23591 if ( $this->is_parent_plugin_installed() ) {
23592 $plugin_fs = self::get_parent_instance();
23593 }
23594
23595 if ( is_object( $plugin_fs ) ) {
23596 if ( ! $plugin_fs->is_registered() ) {
23597 // Forward to parent plugin connect when parent not registered.
23598 $url = $plugin_fs->get_activation_url();
23599 } else {
23600 // Forward to account page.
23601 $url = $plugin_fs->_get_admin_page_url( 'account' );
23602 }
23603 }
23604 }
23605
23606 return $url;
23607 }
23608
23609 /**
23610 * Forward page to activation page.
23611 *
23612 * @author Vova Feldman (@svovaf)
23613 * @since 1.0.3
23614 */
23615 function _redirect_on_activation_hook() {
23616 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
23617 $url = $this->get_after_plugin_activation_redirect_url();
23618
23619 if ( is_string( $url ) ) {
23620 fs_redirect( $url );
23621 }
23622 }
23623 }
23624
23625 /**
23626 * Modify plugin's page action links collection.
23627 *
23628 * @author Vova Feldman (@svovaf)
23629 * @since 1.0.0
23630 *
23631 * @param array $links
23632 * @param $file
23633 *
23634 * @return array
23635 */
23636 function _modify_plugin_action_links_hook( $links, $file ) {
23637 $this->_logger->entrance();
23638
23639 $passed_deactivate = false;
23640 $deactivate_link = '';
23641 $before_deactivate = array();
23642 $after_deactivate = array();
23643 foreach ( $links as $key => $link ) {
23644 if ( 'deactivate' === $key ) {
23645 $deactivate_link = $link;
23646 $passed_deactivate = true;
23647 continue;
23648 }
23649
23650 if ( ! $passed_deactivate ) {
23651 $before_deactivate[ $key ] = $link;
23652 } else {
23653 $after_deactivate[ $key ] = $link;
23654 }
23655 }
23656
23657 ksort( $this->_action_links );
23658
23659 foreach ( $this->_action_links as $new_links ) {
23660 foreach ( $new_links as $link ) {
23661 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
23662 }
23663 }
23664
23665 if ( ! empty( $deactivate_link ) ) {
23666 /**
23667 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
23668 *
23669 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
23670 */
23671 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
23672
23673 // Append deactivation link.
23674 $before_deactivate['deactivate'] = $deactivate_link;
23675 }
23676
23677 return array_merge( $before_deactivate, $after_deactivate );
23678 }
23679
23680 /**
23681 * Adds admin message.
23682 *
23683 * @author Vova Feldman (@svovaf)
23684 * @since 1.0.4
23685 *
23686 * @param string $message
23687 * @param string $title
23688 * @param string $type
23689 */
23690 function add_admin_message( $message, $title = '', $type = 'success' ) {
23691 $this->_admin_notices->add( $message, $title, $type );
23692 }
23693
23694 /**
23695 * Adds sticky admin message.
23696 *
23697 * @author Vova Feldman (@svovaf)
23698 * @since 1.1.0
23699 *
23700 * @param string $message
23701 * @param string $id
23702 * @param string $title
23703 * @param string $type
23704 */
23705 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
23706 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
23707 }
23708
23709 /**
23710 * Check if the paid version of the module is installed.
23711 *
23712 * @author Vova Feldman (@svovaf)
23713 * @since 2.2.0
23714 *
23715 * @return bool
23716 */
23717 private function is_premium_version_installed() {
23718 $premium_plugin_basename = $this->premium_plugin_basename();
23719 $premium_plugin = get_plugins( '/' . dirname( $premium_plugin_basename ) );
23720
23721 return ! empty( $premium_plugin );
23722 }
23723
23724 /**
23725 * Helper function that returns the final steps for the upgrade completion.
23726 *
23727 * If the module is already running the premium code, returns an empty string.
23728 *
23729 * @author Vova Feldman (@svovaf)
23730 * @since 1.2.1
23731 *
23732 * @param string $plan_title
23733 *
23734 * @return string
23735 */
23736 private function get_complete_upgrade_instructions( $plan_title = '' ) {
23737 $this->_logger->entrance();
23738
23739 $activate_license_string = $this->get_license_network_activation_notice();
23740
23741 if ( ! $this->has_premium_version() || $this->is_premium() ) {
23742 return '' . $activate_license_string;
23743 }
23744
23745 if ( empty( $plan_title ) ) {
23746 $plan_title = $this->get_plan_title();
23747 }
23748
23749 if ( $this->is_premium_version_installed() ) {
23750 /**
23751 * If the premium version is already installed, instead of showing the installation instructions,
23752 * tell the current user to activate it.
23753 *
23754 * @author Leo Fajardo (@leorw)
23755 * @since 2.2.1
23756 */
23757 $premium_plugin_basename = $this->premium_plugin_basename();
23758
23759 return sprintf(
23760 /* translators: %1$s: Product title; %2$s: Plan title */
23761 $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' ),
23762 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
23763 $plan_title,
23764 sprintf(
23765 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
23766 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_plugin_basename, 'activate-plugin_' . $premium_plugin_basename ),
23767 esc_html( sprintf(
23768 /* translators: %s: Plan title */
23769 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
23770 $plan_title
23771 ) )
23772 )
23773 );
23774 } else {
23775 // @since 1.2.1.5 The free version is auto deactivated.
23776 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
23777 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
23778 '';
23779
23780 return sprintf(
23781 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
23782 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
23783 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
23784 $this->get_latest_download_link( sprintf(
23785 /* translators: %s: Plan title */
23786 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
23787 $plan_title
23788 ) ),
23789 $deactivation_step,
23790 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
23791 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/upload-wp-' . $this->_module_type . 's' ),
23792 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
23793 );
23794 }
23795 }
23796
23797 /**
23798 * @author Leo Fajardo (@leorw)
23799 * @since 2.1.0
23800 *
23801 * @param string $url
23802 * @param array $request
23803 */
23804 private static function enrich_request_for_debug( &$url, &$request ) {
23805 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
23806 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
23807 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
23808
23809 $request['cookies'] = array(
23810 new WP_Http_Cookie( array(
23811 'name' => 'XDEBUG_SESSION',
23812 'value' => 'PHPSTORM',
23813 ) )
23814 );
23815 }
23816 }
23817
23818 /**
23819 * @author Leo Fajardo (@leorw)
23820 * @since 2.1.0
23821 *
23822 * @param string $url
23823 * @param array $request
23824 * @param int $success_cache_expiration
23825 * @param int $failure_cache_expiration
23826 * @param bool $maybe_enrich_request_for_debug
23827 *
23828 * @return WP_Error|array
23829 */
23830 static function safe_remote_post(
23831 &$url,
23832 $request,
23833 $success_cache_expiration = 0,
23834 $failure_cache_expiration = 0,
23835 $maybe_enrich_request_for_debug = true
23836 ) {
23837 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
23838
23839 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
23840
23841 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
23842 get_transient( $cache_key ) :
23843 false;
23844
23845 if ( false === $response ) {
23846 if ( $maybe_enrich_request_for_debug ) {
23847 self::enrich_request_for_debug( $url, $request );
23848 }
23849
23850 $response = wp_remote_post( $url, $request );
23851
23852 if ( $response instanceof WP_Error ) {
23853 if ( 'https://' === substr( $url, 0, 8 ) &&
23854 isset( $response->errors ) &&
23855 isset( $response->errors['http_request_failed'] )
23856 ) {
23857 $http_error = strtolower( $response->errors['http_request_failed'][0] );
23858
23859 if ( false !== strpos( $http_error, 'ssl' ) ||
23860 false !== strpos( $http_error, 'curl error 35' )
23861 ) {
23862 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
23863 $url = 'http://' . substr( $url, 8 );
23864
23865 $request['timeout'] = 15;
23866 $response = wp_remote_post( $url, $request );
23867 }
23868 }
23869 }
23870
23871 if ( false !== $cache_key ) {
23872 set_transient(
23873 $cache_key,
23874 $response,
23875 ( ( $response instanceof WP_Error ) ?
23876 $failure_cache_expiration :
23877 $success_cache_expiration )
23878 );
23879 }
23880 }
23881
23882 return $response;
23883 }
23884
23885 /**
23886 * This method is used to enrich the after upgrade notice instructions when the upgraded
23887 * license cannot be activated network wide (license quota isn't large enough).
23888 *
23889 * @author Vova Feldman (@svovaf)
23890 * @since 2.0.0
23891 *
23892 * @return string
23893 */
23894 private function get_license_network_activation_notice() {
23895 if ( ! $this->_is_network_active ) {
23896 // Module isn't network level activated.
23897 return '';
23898 }
23899
23900 if ( ! fs_is_network_admin() ) {
23901 // Not network level admin.
23902 return '';
23903 }
23904
23905 if ( get_blog_count() == 1 ) {
23906 // There's only a single site in the network so if there's a context license it was already activated.
23907 return '';
23908 }
23909
23910 if ( ! is_object( $this->_license ) ) {
23911 // No context license.
23912 return '';
23913 }
23914
23915 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
23916 // 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).
23917 return '';
23918 }
23919
23920 if ( $this->can_activate_license_on_network( $this->_license ) ) {
23921 // 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).
23922 return '';
23923 }
23924
23925 return sprintf(
23926 $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' ),
23927 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
23928 '</a>'
23929 );
23930 }
23931
23932 /**
23933 * @author Vova Feldman (@svovaf)
23934 * @since 1.2.1.7
23935 *
23936 * @param string $key
23937 *
23938 * @return string
23939 */
23940 function get_text( $key ) {
23941 return fs_text( $key, $this->_slug );
23942 }
23943
23944 /**
23945 * @author Vova Feldman (@svovaf)
23946 * @since 1.2.3
23947 *
23948 * @param string $text Translatable string.
23949 * @param string $key String key for overrides.
23950 *
23951 * @return string
23952 */
23953 function get_text_inline( $text, $key = '' ) {
23954 return _fs_text_inline( $text, $key, $this->_slug );
23955 }
23956
23957 /**
23958 * @author Vova Feldman (@svovaf)
23959 * @since 1.2.3
23960 *
23961 * @param string $text Translatable string.
23962 * @param string $context Context information for the translators.
23963 * @param string $key String key for overrides.
23964 *
23965 * @return string
23966 */
23967 function get_text_x_inline( $text, $context, $key ) {
23968 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
23969 }
23970
23971 /**
23972 * @author Vova Feldman (@svovaf)
23973 * @since 1.2.3
23974 *
23975 * @param string $text Translatable string.
23976 * @param string $key String key for overrides.
23977 *
23978 * @return string
23979 */
23980 function esc_html_inline( $text, $key ) {
23981 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
23982 }
23983
23984 #----------------------------------------------------------------------------------
23985 #region Versioning
23986 #----------------------------------------------------------------------------------
23987
23988 /**
23989 * Check if Freemius in SDK upgrade mode.
23990 *
23991 * @author Vova Feldman (@svovaf)
23992 * @since 1.0.9
23993 *
23994 * @return bool
23995 */
23996 function is_sdk_upgrade_mode() {
23997 return isset( $this->_storage->sdk_upgrade_mode ) ?
23998 $this->_storage->sdk_upgrade_mode :
23999 false;
24000 }
24001
24002 /**
24003 * Turn SDK upgrade mode off.
24004 *
24005 * @author Vova Feldman (@svovaf)
24006 * @since 1.0.9
24007 */
24008 function set_sdk_upgrade_complete() {
24009 $this->_storage->sdk_upgrade_mode = false;
24010 }
24011
24012 /**
24013 * Check if plugin upgrade mode.
24014 *
24015 * @author Vova Feldman (@svovaf)
24016 * @since 1.0.9
24017 *
24018 * @return bool
24019 */
24020 function is_plugin_upgrade_mode() {
24021 return isset( $this->_storage->plugin_upgrade_mode ) ?
24022 $this->_storage->plugin_upgrade_mode :
24023 false;
24024 }
24025
24026 /**
24027 * Turn plugin upgrade mode off.
24028 *
24029 * @author Vova Feldman (@svovaf)
24030 * @since 1.0.9
24031 */
24032 function set_plugin_upgrade_complete() {
24033 $this->_storage->plugin_upgrade_mode = false;
24034
24035 $license_migration = ! empty( $this->_storage->license_migration ) ?
24036 $this->_storage->license_migration :
24037 array();
24038
24039 $license_migration['is_migrating'] = false;
24040
24041 $this->_storage->license_migration = $license_migration;
24042 }
24043
24044 #endregion
24045
24046 #----------------------------------------------------------------------------------
24047 #region Permissions
24048 #----------------------------------------------------------------------------------
24049
24050 /**
24051 * Check if specific permission requested.
24052 *
24053 * @author Vova Feldman (@svovaf)
24054 * @since 1.1.6
24055 *
24056 * @param string $permission
24057 *
24058 * @return bool
24059 */
24060 function is_permission_requested( $permission ) {
24061 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
24062 }
24063
24064 #endregion
24065
24066 #----------------------------------------------------------------------------------
24067 #region Auto Activation
24068 #----------------------------------------------------------------------------------
24069
24070 /**
24071 * Hints the SDK if running an auto-installation.
24072 *
24073 * @var bool
24074 */
24075 private $_isAutoInstall = false;
24076
24077 /**
24078 * After upgrade callback to install and auto activate a plugin.
24079 * This code will only be executed on explicit request from the user,
24080 * following the practice Jetpack are using with their theme installations.
24081 *
24082 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
24083 *
24084 * @author Vova Feldman (@svovaf)
24085 * @since 1.2.1.7
24086 */
24087 function _install_premium_version_ajax_action() {
24088 $this->_logger->entrance();
24089
24090 $this->check_ajax_referer( 'install_premium_version' );
24091
24092 if ( ! $this->is_registered() ) {
24093 // Not registered.
24094 self::shoot_ajax_failure( array(
24095 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
24096 'code' => 'premium_installed',
24097 ) );
24098 }
24099
24100 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
24101
24102 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24103 // Invalid ID.
24104 self::shoot_ajax_failure( array(
24105 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24106 'code' => 'invalid_module_id',
24107 ) );
24108 }
24109
24110 if ( $plugin_id == $this->get_id() ) {
24111 if ( $this->is_premium() ) {
24112 // Already using the premium code version.
24113 self::shoot_ajax_failure( array(
24114 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
24115 'code' => 'premium_installed',
24116 ) );
24117 }
24118 if ( ! $this->can_use_premium_code() ) {
24119 // Don't have access to the premium code.
24120 self::shoot_ajax_failure( array(
24121 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
24122 'code' => 'invalid_license',
24123 ) );
24124 }
24125 if ( ! $this->has_release_on_freemius() ) {
24126 // Plugin is a serviceware, no premium code version.
24127 self::shoot_ajax_failure( array(
24128 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
24129 'code' => 'premium_version_missing',
24130 ) );
24131 }
24132 } else {
24133 $addon = $this->get_addon( $plugin_id );
24134
24135 if ( ! is_object( $addon ) ) {
24136 // Invalid add-on ID.
24137 self::shoot_ajax_failure( array(
24138 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24139 'code' => 'invalid_module_id',
24140 ) );
24141 }
24142
24143 if ( $this->is_addon_activated( $plugin_id, true ) ) {
24144 // Premium add-on version is already activated.
24145 self::shoot_ajax_failure( array(
24146 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
24147 'code' => 'premium_installed',
24148 ) );
24149 }
24150 }
24151
24152 $this->_isAutoInstall = true;
24153
24154 // Try to install and activate.
24155 $updater = FS_Plugin_Updater::instance( $this );
24156 $result = $updater->install_and_activate_plugin( $plugin_id );
24157
24158 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
24159 self::shoot_ajax_failure( array(
24160 'message' => $result['message'],
24161 'code' => $result['code'],
24162 ) );
24163 }
24164
24165 self::shoot_ajax_success( $result );
24166 }
24167
24168 /**
24169 * Displays module activation dialog box after a successful upgrade
24170 * where the user explicitly requested to auto download and install
24171 * the premium version.
24172 *
24173 * @author Vova Feldman (@svovaf)
24174 * @since 1.2.1.7
24175 */
24176 function _add_auto_installation_dialog_box() {
24177 $this->_logger->entrance();
24178
24179 if ( ! $this->is_registered() ) {
24180 // Not registered.
24181 return;
24182 }
24183
24184 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
24185
24186 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24187 // Invalid module ID.
24188 return;
24189 }
24190
24191 if ( $plugin_id == $this->get_id() ) {
24192 if ( $this->is_premium() ) {
24193 // Already using the premium code version.
24194 return;
24195 }
24196 if ( ! $this->can_use_premium_code() ) {
24197 // Don't have access to the premium code.
24198 return;
24199 }
24200 if ( ! $this->has_release_on_freemius() ) {
24201 // Plugin is a serviceware, no premium code version.
24202 return;
24203 }
24204 } else {
24205 $addon = $this->get_addon( $plugin_id );
24206
24207 if ( ! is_object( $addon ) ) {
24208 // Invalid add-on ID.
24209 return;
24210 }
24211
24212 if ( $this->is_addon_activated( $plugin_id, true ) ) {
24213 // Premium add-on version is already activated.
24214 return;
24215 }
24216 }
24217
24218 $vars = array(
24219 'id' => $this->_module_id,
24220 'target_module_id' => $plugin_id,
24221 'slug' => $this->_slug,
24222 );
24223
24224 fs_require_template( 'auto-installation.php', $vars );
24225 }
24226
24227 #endregion
24228
24229 #--------------------------------------------------------------------------------
24230 #region Tabs Integration
24231 #--------------------------------------------------------------------------------
24232
24233 #region Module's Original Tabs
24234
24235 /**
24236 * Inject a JavaScript logic to capture the theme tabs HTML.
24237 *
24238 * @author Vova Feldman (@svovaf)
24239 * @since 1.2.2.7
24240 */
24241 function _tabs_capture() {
24242 $this->_logger->entrance();
24243
24244 if ( ! $this->is_product_settings_page() ||
24245 ! $this->is_matching_url( $this->main_menu_url() )
24246 ) {
24247 return;
24248 }
24249
24250 $params = array(
24251 'id' => $this->_module_id,
24252 );
24253
24254 fs_require_once_template( 'tabs-capture-js.php', $params );
24255 }
24256
24257 /**
24258 * Cache theme's tabs HTML for a week. The cache will also be set as expired
24259 * after version and type (free/premium) changes, in addition to the week period.
24260 *
24261 * @author Vova Feldman (@svovaf)
24262 * @since 1.2.2.7
24263 */
24264 function _store_tabs_ajax_action() {
24265 $this->_logger->entrance();
24266
24267 $this->check_ajax_referer( 'store_tabs' );
24268
24269 // Init filesystem if not yet initiated.
24270 WP_Filesystem();
24271
24272 // Get POST body HTML data.
24273 global $wp_filesystem;
24274 $tabs_html = $wp_filesystem->get_contents( "php://input" );
24275
24276 if ( is_string( $tabs_html ) ) {
24277 $tabs_html = trim( $tabs_html );
24278 }
24279
24280 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
24281 self::shoot_ajax_failure();
24282 }
24283
24284 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
24285
24286 self::shoot_ajax_success();
24287 }
24288
24289 /**
24290 * Cache theme's settings page custom styles. The cache will also be set as expired
24291 * after version and type (free/premium) changes, in addition to the week period.
24292 *
24293 * @author Vova Feldman (@svovaf)
24294 * @since 1.2.2.7
24295 */
24296 function _store_tabs_styles() {
24297 $this->_logger->entrance();
24298
24299 if ( ! $this->is_product_settings_page() ||
24300 ! $this->is_matching_url( $this->main_menu_url() )
24301 ) {
24302 return;
24303 }
24304
24305 $wp_styles = wp_styles();
24306
24307 $theme_styles_url = get_template_directory_uri();
24308
24309 $stylesheets = array();
24310 foreach ( $wp_styles->queue as $handler ) {
24311 if ( fs_starts_with( $handler, 'fs_' ) ) {
24312 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
24313 continue;
24314 }
24315
24316 /**
24317 * @var _WP_Dependency $stylesheet
24318 */
24319 $stylesheet = $wp_styles->registered[ $handler ];
24320
24321 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
24322 $stylesheets[] = $stylesheet->src;
24323 }
24324 }
24325
24326 if ( ! empty( $stylesheets ) ) {
24327 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
24328 }
24329 }
24330
24331 /**
24332 * Check if module's original settings page has any tabs.
24333 *
24334 * @author Vova Feldman (@svovaf)
24335 * @since 1.2.2.7
24336 *
24337 * @return bool
24338 */
24339 private function has_tabs() {
24340 return $this->_cache->has( 'tabs' );
24341 }
24342
24343 /**
24344 * Get module's settings page HTML content, starting
24345 * from the beginning of the <div class="wrap"> element,
24346 * until the tabs HTML (including).
24347 *
24348 * @author Vova Feldman (@svovaf)
24349 * @since 1.2.2.7
24350 *
24351 * @return string
24352 */
24353 private function get_tabs_html() {
24354 $this->_logger->entrance();
24355
24356 return $this->_cache->get( 'tabs' );
24357 }
24358
24359 /**
24360 * Check if page should include tabs.
24361 *
24362 * @author Vova Feldman (@svovaf)
24363 * @since 1.2.2.7
24364 *
24365 * @return bool
24366 */
24367 private function should_page_include_tabs() {
24368 if ( ! $this->has_settings_menu() ) {
24369 // Don't add tabs if no settings at all.
24370 return false;
24371 }
24372
24373 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
24374 // Only add tabs to themes for now.
24375 return false;
24376 }
24377
24378 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
24379 // Only add tabs to monetizing themes.
24380 return false;
24381 }
24382
24383 if ( ! $this->is_product_settings_page() ) {
24384 // Only add tabs if browsing one of the product's setting pages.
24385 return false;
24386 }
24387
24388 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
24389 // Don't include tabs in the activation page.
24390 return false;
24391 }
24392
24393 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
24394 // Don't add tabs on checkout page, we want to reduce distractions
24395 // as much as possible.
24396 return false;
24397 }
24398
24399 return true;
24400 }
24401
24402 /**
24403 * Add the tabs HTML before the setting's page content and
24404 * enqueue any required stylesheets.
24405 *
24406 * @author Vova Feldman (@svovaf)
24407 * @since 1.2.2.7
24408 *
24409 * @return bool If tabs were included.
24410 */
24411 function _add_tabs_before_content() {
24412 $this->_logger->entrance();
24413
24414 if ( ! $this->should_page_include_tabs() ) {
24415 return false;
24416 }
24417
24418 /**
24419 * Enqueue the original stylesheets that are included in the
24420 * theme settings page. That way, if the theme settings has
24421 * some custom _styled_ content above the tabs UI, this
24422 * will make sure that the styling is preserved.
24423 */
24424 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
24425 if ( is_array( $stylesheets ) ) {
24426 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
24427 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
24428 }
24429 }
24430
24431 // Cut closing </div> tag.
24432 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
24433
24434 return true;
24435 }
24436
24437 /**
24438 * Add the tabs closing HTML after the setting's page content.
24439 *
24440 * @author Vova Feldman (@svovaf)
24441 * @since 1.2.2.7
24442 *
24443 * @return bool If tabs closing HTML was included.
24444 */
24445 function _add_tabs_after_content() {
24446 $this->_logger->entrance();
24447
24448 if ( ! $this->should_page_include_tabs() ) {
24449 return false;
24450 }
24451
24452 echo '</div>';
24453
24454 return true;
24455 }
24456
24457 #endregion
24458
24459 /**
24460 * Add in-page JavaScript to inject the Freemius tabs into
24461 * the module's setting tabs section.
24462 *
24463 * @author Vova Feldman (@svovaf)
24464 * @since 1.2.2.7
24465 */
24466 function _add_freemius_tabs() {
24467 $this->_logger->entrance();
24468
24469 if ( ! $this->should_page_include_tabs() ) {
24470 return;
24471 }
24472
24473 $params = array( 'id' => $this->_module_id );
24474 fs_require_once_template( 'tabs.php', $params );
24475 }
24476
24477 #endregion
24478
24479 #--------------------------------------------------------------------------------
24480 #region Customizer Integration for Themes
24481 #--------------------------------------------------------------------------------
24482
24483 /**
24484 * @author Vova Feldman (@svovaf)
24485 * @since 1.2.2.7
24486 *
24487 * @param WP_Customize_Manager $customizer
24488 */
24489 function _customizer_register( $customizer ) {
24490 $this->_logger->entrance();
24491
24492 if ( $this->is_pricing_page_visible() ) {
24493 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
24494
24495 $customizer->add_section( 'freemius_upsell', array(
24496 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
24497 'priority' => 1,
24498 ) );
24499 $customizer->add_setting( 'freemius_upsell', array(
24500 'sanitize_callback' => 'esc_html',
24501 ) );
24502
24503 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
24504 'fs' => $this,
24505 'section' => 'freemius_upsell',
24506 'priority' => 100,
24507 ) ) );
24508 }
24509
24510 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
24511 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
24512
24513 // Main Documentation Link In Customizer Root.
24514 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
24515 'fs' => $this,
24516 'priority' => 1000,
24517 ) ) );
24518 }
24519 }
24520
24521 #endregion
24522
24523 /**
24524 * If the theme has a paid version, add some custom
24525 * styling to the theme's premium version (if exists)
24526 * to highlight that it's the premium version of the
24527 * same theme, making it easier for identification
24528 * after the user upgrades and upload it to the site.
24529 *
24530 * @author Vova Feldman (@svovaf)
24531 * @since 1.2.2.7
24532 */
24533 function _style_premium_theme() {
24534 $this->_logger->entrance();
24535
24536 if ( ! self::is_themes_page() ) {
24537 // Only include in the themes page.
24538 return;
24539 }
24540
24541 if ( ! $this->has_paid_plan() ) {
24542 // Only include if has any paid plans.
24543 return;
24544 }
24545
24546 $params = null;
24547 fs_require_once_template( '/js/jquery.content-change.php', $params );
24548
24549 $params = array(
24550 'slug' => $this->_slug,
24551 'id' => $this->_module_id,
24552 );
24553
24554 fs_require_template( '/js/style-premium-theme.php', $params );
24555 }
24556
24557 /**
24558 * This method will return the absolute URL of the module's local icon.
24559 *
24560 * When you are running your plugin or theme on a **localhost** environment, if the icon
24561 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
24562 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
24563 * If an icon is found, this method will automatically attempt to download the icon and store it
24564 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
24565 *
24566 * It's important to mention that this method is NOT phoning home since the developer will deploy
24567 * the product with the local icon in the assets folder. The download process just simplifies
24568 * the process for the developer.
24569 *
24570 * @author Vova Feldman (@svovaf)
24571 * @since 2.0.0
24572 *
24573 * @return string
24574 */
24575 function get_local_icon_url() {
24576 global $fs_active_plugins;
24577
24578 /**
24579 * @since 1.1.7.5
24580 */
24581 $local_path = $this->apply_filters( 'plugin_icon', false );
24582
24583 if ( is_string( $local_path ) ) {
24584 $icons = array( $local_path );
24585 } else {
24586 $img_dir = WP_FS__DIR_IMG;
24587
24588 // Locate the main assets folder.
24589 if ( 1 < count( $fs_active_plugins->plugins ) ) {
24590 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
24591
24592 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
24593 if ( $data->plugin_path == $this->get_plugin_basename() ) {
24594 $img_dir = $plugin_or_theme_img_dir
24595 . '/'
24596 /**
24597 * The basename will be `themes` or the basename of a custom themes directory.
24598 *
24599 * @author Leo Fajardo (@leorw)
24600 * @since 2.2.3
24601 */
24602 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
24603 . '/assets/img';
24604
24605 break;
24606 }
24607 }
24608 }
24609
24610 // Try to locate the icon in the assets folder.
24611 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
24612
24613 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
24614 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
24615 $icons = array(
24616 fs_normalize_path( $img_dir . '/theme-icon.png' )
24617 );
24618 } else {
24619 $icon_found = false;
24620 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
24621
24622 if ( ! function_exists( 'get_filesystem_method' ) ) {
24623 require_once ABSPATH . 'wp-admin/includes/file.php';
24624 }
24625
24626 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
24627
24628 /**
24629 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
24630 *
24631 * This code will only be executed once during the testing
24632 * of the plugin in a local environment. The plugin icon file WILL
24633 * already exist in the assets folder when the plugin is deployed to
24634 * the repository.
24635 */
24636 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
24637 // Fetch icon from Freemius.
24638 $icon = $this->fetch_remote_icon_url();
24639
24640 // Fetch icon from WordPress.org.
24641 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
24642 if ( ! function_exists( 'plugins_api' ) ) {
24643 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
24644 }
24645
24646 $plugin_information = plugins_api( 'plugin_information', array(
24647 'slug' => $this->_slug,
24648 'fields' => array(
24649 'sections' => false,
24650 'tags' => false,
24651 'icons' => true
24652 )
24653 ) );
24654
24655 if (
24656 ! is_wp_error( $plugin_information )
24657 && isset( $plugin_information->icons )
24658 && ! empty( $plugin_information->icons )
24659 ) {
24660 /**
24661 * Get the smallest icon.
24662 *
24663 * @author Leo Fajardo (@leorw)
24664 * @since 1.2.2
24665 */
24666 $icon = end( $plugin_information->icons );
24667 }
24668 }
24669
24670 if ( ! empty( $icon ) ) {
24671 if ( 0 !== strpos( $icon, 'http' ) ) {
24672 $icon = 'http:' . $icon;
24673 }
24674
24675 /**
24676 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
24677 *
24678 * @author Leo Fajardo (@leorw)
24679 * @since 1.2.2
24680 */
24681 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
24682
24683 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
24684
24685 // Try to download the icon.
24686 $icon_found = fs_download_image( $icon, $local_path );
24687 }
24688 }
24689
24690 if ( ! $icon_found ) {
24691 // No icons found, fallback to default icon.
24692 if ( $have_write_permissions ) {
24693 // If have write permissions, copy default icon.
24694 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
24695 } else {
24696 // If doesn't have write permissions, use default icon path.
24697 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
24698 }
24699 }
24700
24701 $icons = array( $local_path );
24702 }
24703 }
24704 }
24705
24706 $icon_dir = dirname( $icons[0] );
24707
24708 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
24709 }
24710
24711 /**
24712 * Fetch module's extended info.
24713 *
24714 * @author Vova Feldman (@svovaf)
24715 * @since 2.0.0
24716 *
24717 * @return object|mixed
24718 */
24719 private function fetch_module_info() {
24720 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
24721 }
24722
24723 /**
24724 * Fetch module's remote icon URL.
24725 *
24726 * @author Vova Feldman (@svovaf)
24727 * @since 2.0.0
24728 *
24729 * @return string
24730 */
24731 function fetch_remote_icon_url() {
24732 $info = $this->fetch_module_info();
24733
24734 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
24735 $info->icon :
24736 '';
24737 }
24738
24739 #--------------------------------------------------------------------------------
24740 #region GDPR
24741 #--------------------------------------------------------------------------------
24742
24743 /**
24744 * @author Leo Fajardo (@leorw)
24745 * @since 2.1.0
24746 *
24747 * @return bool
24748 */
24749 function fetch_and_store_current_user_gdpr_anonymously() {
24750 $pong = $this->ping( null, true );
24751
24752 if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
24753 return false;
24754 } else {
24755 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
24756
24757 return $pong->is_gdpr_required;
24758 }
24759 }
24760
24761 /**
24762 * @author Leo Fajardo (@leorw)
24763 * @since 2.1.0
24764 *
24765 * @param array $user_plugins
24766 *
24767 * @return string
24768 */
24769 private function get_gdpr_admin_notice_string( $user_plugins ) {
24770 $this->_logger->entrance();
24771
24772 $addons = self::get_all_addons();
24773
24774 foreach ( $user_plugins as $user_plugin ) {
24775 $has_addons = isset( $addons[ $user_plugin->id ] );
24776
24777 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
24778 if ( $this->_module_id == $user_plugin->id ) {
24779 $addons = $this->get_addons();
24780 $has_addons = ( ! empty( $addons ) );
24781 } else {
24782 $plugin_api = FS_Api::instance(
24783 $user_plugin->id,
24784 'plugin',
24785 $user_plugin->id,
24786 $user_plugin->public_key,
24787 ! $user_plugin->is_live,
24788 false,
24789 $this->get_sdk_version()
24790 );
24791
24792 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
24793
24794 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
24795 is_array( $addons_result->plugins ) &&
24796 ! empty( $addons_result->plugins )
24797 ) {
24798 $has_addons = true;
24799 }
24800 }
24801 }
24802
24803 $user_plugin->has_addons = $has_addons;
24804 }
24805
24806 $is_single_parent_product = ( 1 === count( $user_plugins ) );
24807
24808 $multiple_products_text = '';
24809
24810 if ( $is_single_parent_product ) {
24811 $single_parent_product = reset( $user_plugins );
24812
24813 $thank_you = sprintf(
24814 "<span data-plugin-id='%d'>%s</span>",
24815 $single_parent_product->id,
24816 sprintf(
24817 $single_parent_product->has_addons ?
24818 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
24819 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
24820 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
24821 )
24822 );
24823
24824 $already_opted_in = sprintf(
24825 $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' ),
24826 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
24827 );
24828 } else {
24829 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
24830 $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' );
24831
24832 $products_and_add_ons = '';
24833 foreach ( $user_plugins as $user_plugin ) {
24834 if ( ! empty( $products_and_add_ons ) ) {
24835 $products_and_add_ons .= ', ';
24836 }
24837
24838 if ( ! $user_plugin->has_addons ) {
24839 $products_and_add_ons .= sprintf(
24840 "<span data-plugin-id='%d'>%s</span>",
24841 $user_plugin->id,
24842 $user_plugin->title
24843 );
24844 } else {
24845 $products_and_add_ons .= sprintf(
24846 "<span data-plugin-id='%d'>%s</span>",
24847 $user_plugin->id,
24848 sprintf(
24849 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
24850 $user_plugin->title
24851 )
24852 );
24853 }
24854 }
24855
24856 $multiple_products_text = sprintf(
24857 "<small class='products'><strong>%s:</strong> %s</small>",
24858 $this->get_text_inline( 'Products', 'products' ),
24859 $products_and_add_ons
24860 );
24861 }
24862
24863 $actions = sprintf(
24864 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
24865 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
24866 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
24867 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
24868 sprintf(
24869 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
24870 '<span class="underlined">',
24871 '</span>'
24872 )
24873 );
24874
24875 return sprintf(
24876 '%s %s %s',
24877 $thank_you,
24878 $already_opted_in,
24879 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>' ) .
24880 '<br><br>' .
24881 '<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>' .
24882 $actions .
24883 ( $is_single_parent_product ? '' : $multiple_products_text )
24884 );
24885 }
24886
24887 /**
24888 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
24889 * plugins and themes they've opted in to.
24890 *
24891 * @author Leo Fajardo (@leorw)
24892 * @since 2.1.0
24893 *
24894 * @param string $user_email
24895 * @param string $license_key
24896 * @param array $plugin_ids
24897 * @param string|null $license_key
24898 *
24899 * @return array|false
24900 */
24901 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
24902 $request = array(
24903 'method' => 'POST',
24904 'body' => array(),
24905 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
24906 );
24907
24908 if ( is_string( $user_email ) ) {
24909 $request['body']['email'] = $user_email;
24910 } else {
24911 $request['body']['license_key'] = $license_key;
24912 }
24913
24914 $result = array();
24915
24916 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
24917 $total_plugin_ids = count( $plugin_ids );
24918
24919 $plugin_ids_count_per_request = 10;
24920 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
24921 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
24922
24923 $request['body']['plugin_ids'] = $plugin_ids_set;
24924
24925 $response = self::safe_remote_post(
24926 $url,
24927 $request,
24928 WP_FS__TIME_24_HOURS_IN_SEC,
24929 WP_FS__TIME_12_HOURS_IN_SEC
24930 );
24931
24932 if ( ! is_wp_error( $response ) ) {
24933 $decoded = is_string( $response['body'] ) ?
24934 json_decode( $response['body'] ) :
24935 null;
24936
24937 if (
24938 !is_object($decoded) ||
24939 !isset($decoded->success) ||
24940 true !== $decoded->success ||
24941 !isset( $decoded->data ) ||
24942 !is_array( $decoded->data )
24943 ) {
24944 return false;
24945 }
24946
24947 $result = array_merge( $result, $decoded->data );
24948 }
24949 }
24950
24951 return $result;
24952 }
24953
24954 /**
24955 * @author Leo Fajardo (@leorw)
24956 * @since 2.1.0
24957 */
24958 function _maybe_show_gdpr_admin_notice() {
24959 if ( ! $this->is_user_in_admin() ) {
24960 return;
24961 }
24962
24963 if ( ! $this->should_handle_gdpr_admin_notice() ) {
24964 return;
24965 }
24966
24967 if ( ! $this->is_user_admin() ) {
24968 return;
24969 }
24970
24971 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
24972
24973 $lock = FS_User_Lock::instance();
24974
24975 /**
24976 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
24977 */
24978 if ( ! $lock->try_lock( 60 ) ) {
24979 return;
24980 }
24981
24982 /**
24983 * @var $current_wp_user WP_User
24984 */
24985 $current_wp_user = self::_get_current_wp_user();
24986
24987 /**
24988 * @var FS_User $current_fs_user
24989 */
24990 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
24991
24992 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
24993
24994 if ( ! is_object( $current_fs_user ) ) {
24995 // 10-year lock.
24996 $lock->lock( $ten_years_in_sec );
24997
24998 return;
24999 }
25000
25001 $gdpr = FS_GDPR_Manager::instance();
25002
25003 if ( $gdpr->is_opt_in_notice_shown() ) {
25004 // 30-day lock.
25005 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25006
25007 return;
25008 }
25009
25010 if ( ! $gdpr->should_show_opt_in_notice() ) {
25011 // 10-year lock.
25012 $lock->lock( $ten_years_in_sec );
25013
25014 return;
25015 }
25016
25017 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
25018 $was_notice_shown_before = ( false !== $last_time_notice_shown );
25019
25020 if ( $was_notice_shown_before &&
25021 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
25022 ) {
25023 // If the notice was shown before, show it again after 30 days from the last time it was shown.
25024 return;
25025 }
25026
25027 /**
25028 * Find all plugin IDs that were installed by the current admin.
25029 */
25030 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
25031
25032 if ( empty( $plugin_ids_map )) {
25033 $lock->lock( $ten_years_in_sec );
25034
25035 return;
25036 }
25037
25038 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25039 $current_fs_user->email,
25040 null,
25041 array_keys( $plugin_ids_map )
25042 );
25043
25044 if ( empty( $user_plugins ) ) {
25045 $lock->lock(
25046 is_array($user_plugins) ?
25047 $ten_years_in_sec :
25048 // Lock for 24-hours on errors.
25049 WP_FS__TIME_24_HOURS_IN_SEC
25050 );
25051
25052 return;
25053 }
25054
25055 $has_unset_marketing_optin = false;
25056
25057 foreach ( $user_plugins as $user_plugin ) {
25058 if ( true == $user_plugin->is_marketing_allowed ) {
25059 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
25060 }
25061
25062 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
25063 $has_unset_marketing_optin = true;
25064 }
25065 }
25066
25067 if ( empty( $plugin_ids_map ) ||
25068 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
25069 ) {
25070 $lock->lock( $ten_years_in_sec );
25071
25072 return;
25073 }
25074
25075 $modules = array_merge(
25076 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25077 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25078 );
25079
25080 foreach ( $modules as $module ) {
25081 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
25082 $plugin_ids_map[ $module->id ] = $module;
25083 }
25084 }
25085
25086 $plugin_title = null;
25087 if ( 1 === count( $plugin_ids_map ) ) {
25088 $module = reset( $plugin_ids_map );
25089 $plugin_title = $module->title;
25090 }
25091
25092 $gdpr->add_opt_in_sticky_notice(
25093 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
25094 $plugin_title
25095 );
25096
25097 $this->add_gdpr_optin_ajax_handler_and_style();
25098
25099 $gdpr->notice_was_just_shown();
25100
25101 // 30-day lock.
25102 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25103 }
25104
25105 /**
25106 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
25107 * marketing.
25108 *
25109 * @author Leo Fajardo (@leorw)
25110 * @since 2.1.0
25111 */
25112 private function disable_opt_in_notice_and_lock_user() {
25113 FS_GDPR_Manager::instance()->disable_opt_in_notice();
25114
25115 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25116
25117 // 10-year lock.
25118 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
25119 }
25120
25121 /**
25122 * @author Leo Fajardo (@leorw)
25123 * @since 2.1.0
25124 */
25125 function _add_gdpr_optin_js() {
25126 $vars = array( 'id' => $this->_module_id );
25127
25128 fs_require_once_template( 'gdpr-optin-js.php', $vars );
25129 }
25130
25131 /**
25132 * @author Leo Fajardo (@leorw)
25133 * @since 2.1.0
25134 */
25135 function enqueue_gdpr_optin_notice_style() {
25136 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
25137 }
25138
25139 /**
25140 * @author Leo Fajardo (@leorw)
25141 * @since 2.1.0
25142 */
25143 function _maybe_add_gdpr_optin_ajax_handler() {
25144 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
25145
25146 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
25147 $this->add_gdpr_optin_ajax_handler_and_style();
25148 }
25149 }
25150
25151 /**
25152 * @author Leo Fajardo (@leorw)
25153 * @since 2.1.0
25154 */
25155 function _fetch_is_marketing_required_flag_value_ajax_action() {
25156 $this->_logger->entrance();
25157
25158 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
25159
25160 $license_key = fs_request_get( 'license_key' );
25161
25162 if ( empty($license_key) ) {
25163 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
25164 }
25165
25166 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25167 null,
25168 $license_key,
25169 array( $this->_module_id )
25170 );
25171
25172 if ( ! is_array( $user_plugins ) ||
25173 empty($user_plugins) ||
25174 !isset($user_plugins[0]->plugin_id) ||
25175 $user_plugins[0]->plugin_id != $this->_module_id
25176 ) {
25177 /**
25178 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
25179 *
25180 * @author Vova Feldman (@svovaf)
25181 */
25182 self::shoot_ajax_success( array(
25183 'is_marketing_allowed' => null,
25184 'license_owner_id' => null
25185 ) );
25186 }
25187
25188 self::shoot_ajax_success( array(
25189 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
25190 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
25191 ) );
25192 }
25193
25194 /**
25195 * @author Leo Fajardo (@leorw)
25196 * @since 2.3.2
25197 *
25198 * @param number[] $install_ids
25199 *
25200 * @return array {
25201 * An array of objects containing the installs' licenses owners data.
25202 *
25203 * @property number $id User ID.
25204 * @property string $email User email (can be masked email).
25205 * }
25206 */
25207 private function fetch_installs_licenses_owners_data( $install_ids ) {
25208 $this->_logger->entrance();
25209
25210 $response = $this->get_api_user_scope()->get(
25211 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
25212 );
25213
25214 $license_owners = null;
25215
25216 if ( $this->is_api_result_object( $response, 'owners' ) ) {
25217 $license_owners = $response->owners;
25218 }
25219
25220 return $license_owners;
25221 }
25222
25223 /**
25224 * @author Leo Fajardo (@leorw)
25225 * @since 2.1.0
25226 */
25227 private function add_gdpr_optin_ajax_handler_and_style() {
25228 // Add GDPR action AJAX callback.
25229 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
25230
25231 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
25232 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
25233 }
25234
25235 /**
25236 * @author Leo Fajardo (@leorw)
25237 * @since 2.1.0
25238 */
25239 function _gdpr_optin_ajax_action() {
25240 $this->_logger->entrance();
25241
25242 $this->check_ajax_referer( 'gdpr_optin_action' );
25243
25244 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
25245 self::shoot_ajax_failure();
25246 }
25247
25248 $current_wp_user = self::_get_current_wp_user();
25249
25250 $plugin_ids = fs_request_get( 'plugin_ids', array() );
25251 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
25252 self::shoot_ajax_failure();
25253 }
25254
25255 $modules = array_merge(
25256 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25257 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25258 );
25259
25260 foreach ( $modules as $key => $module ) {
25261 if ( ! in_array( $module->id, $plugin_ids ) ) {
25262 unset( $modules[ $key ] );
25263 }
25264 }
25265
25266 if ( empty( $modules ) ) {
25267 self::shoot_ajax_failure();
25268 }
25269
25270 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
25271
25272 foreach ( $modules as $module ) {
25273 $user_api->call( "?plugin_id={$module->id}", 'put', array(
25274 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
25275 ) );
25276 }
25277
25278 FS_GDPR_Manager::instance()->remove_opt_in_notice();
25279
25280 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25281
25282 // 10-year lock.
25283 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
25284
25285 self::shoot_ajax_success();
25286 }
25287
25288 /**
25289 * 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.
25290 *
25291 * @author Vova Feldman (@svovaf)
25292 * @since 2.1.0
25293 *
25294 * @return bool
25295 */
25296 private function should_handle_gdpr_admin_notice() {
25297 return $this->apply_filters(
25298 'handle_gdpr_admin_notice',
25299 // Default to false.
25300 false
25301 );
25302 }
25303
25304 #endregion
25305
25306 #----------------------------------------------------------------------------------
25307 #region Marketing
25308 #----------------------------------------------------------------------------------
25309
25310 /**
25311 * Check if current user purchased any other plugins before.
25312 *
25313 * @author Vova Feldman (@svovaf)
25314 * @since 1.0.9
25315 *
25316 * @return bool
25317 */
25318 function has_purchased_before() {
25319 // TODO: Implement has_purchased_before() method.
25320 throw new Exception( 'not implemented' );
25321 }
25322
25323 /**
25324 * Check if current user classified as an agency.
25325 *
25326 * @author Vova Feldman (@svovaf)
25327 * @since 1.0.9
25328 *
25329 * @return bool
25330 */
25331 function is_agency() {
25332 // TODO: Implement is_agency() method.
25333 throw new Exception( 'not implemented' );
25334 }
25335
25336 /**
25337 * Check if current user classified as a developer.
25338 *
25339 * @author Vova Feldman (@svovaf)
25340 * @since 1.0.9
25341 *
25342 * @return bool
25343 */
25344 function is_developer() {
25345 // TODO: Implement is_developer() method.
25346 throw new Exception( 'not implemented' );
25347 }
25348
25349 /**
25350 * Check if current user classified as a business.
25351 *
25352 * @author Vova Feldman (@svovaf)
25353 * @since 1.0.9
25354 *
25355 * @return bool
25356 */
25357 function is_business() {
25358 // TODO: Implement is_business() method.
25359 throw new Exception( 'not implemented' );
25360 }
25361
25362 #endregion
25363
25364 #----------------------------------------------------------------------------------
25365 #region Helper
25366 #----------------------------------------------------------------------------------
25367
25368 /**
25369 * If running with a secret key, assume it's the developer and show pending plans as well.
25370 *
25371 * @author Vova Feldman (@svovaf)
25372 * @since 2.1.2
25373 *
25374 * @param string $path
25375 *
25376 * @return string
25377 */
25378 function add_show_pending( $path ) {
25379 if ( ! $this->has_secret_key() ) {
25380 return $path;
25381 }
25382
25383 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
25384 }
25385
25386 #endregion
25387 }
25388