PluginProbe
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites / 1.3.2
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites v1.3.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 1.3.2, at freemius/includes/class-freemius.php

25,327 lines 955.1 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 #endregion
389
390 /* Ctor
391 ------------------------------------------------------------------------------------------------------------------*/
392
393 /**
394 * Main singleton instance.
395 *
396 * @author Vova Feldman (@svovaf)
397 * @since 1.0.0
398 *
399 * @param number $module_id
400 * @param string|bool $slug
401 * @param bool $is_init Since 1.2.1 Is initiation sequence.
402 */
403 private function __construct( $module_id, $slug = false, $is_init = false ) {
404 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
405 $this->store_id_slug_type_path_map( $module_id, $slug );
406 }
407
408 $this->_module_id = $module_id;
409 $this->_slug = $this->get_slug();
410 $this->_module_type = $this->get_module_type();
411
412 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
413
414 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
415
416 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
417
418 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
419
420 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init );
421 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
422 $this->_plugin_basename = $this->get_plugin_basename();
423 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
424
425 $this->_is_multisite_integrated = (
426 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
427 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
428 );
429
430 $this->_is_network_active = (
431 is_multisite() &&
432 $this->_is_multisite_integrated &&
433 // Themes are always network activated, but the ACTUAL activation is per site.
434 $this->is_plugin() &&
435 (
436 is_plugin_active_for_network( $this->_plugin_basename ) ||
437 // Plugin network level activation or uninstall.
438 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
439 )
440 );
441
442 $this->_storage->set_network_active(
443 $this->_is_network_active,
444 $this->is_delegated_connection()
445 );
446
447 if ( ! isset( $this->_storage->is_network_activated ) ) {
448 $this->_storage->is_network_activated = $this->_is_network_active;
449 }
450
451 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
452 // Update last activation level.
453 $this->_storage->is_network_activated = $this->_is_network_active;
454
455 $this->maybe_adjust_storage();
456 }
457
458 #region Migration
459
460 if ( is_multisite() ) {
461 /**
462 * If the install_timestamp exists on the site level but doesn't exist on the
463 * network level storage, it means that we need to process the storage with migration.
464 *
465 * 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.
466 *
467 * @author Vova Feldman (@svovaf)
468 * @since 2.0.0
469 */
470 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
471 false !== $this->_storage->get( 'install_timestamp', false, false )
472 ) {
473 // Initiate storage migration.
474 $this->_storage->migrate_to_network();
475
476 // Migrate module cache to network level storage.
477 $this->_cache->migrate_to_network();
478 }
479 }
480
481 #endregion
482
483 $base_name_split = explode( '/', $this->_plugin_basename );
484 $this->_plugin_dir_name = $base_name_split[0];
485
486 if ( $this->_logger->is_on() ) {
487 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
488 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
489 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
490 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
491 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
492 }
493
494 // Remember link between file to slug.
495 $this->store_file_slug_map();
496
497 // Store plugin's initial install timestamp.
498 if ( ! isset( $this->_storage->install_timestamp ) ) {
499 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
500 }
501
502 if ( ! is_object( $this->_plugin ) ) {
503 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
504 }
505
506 $this->_admin_notices = FS_Admin_Notices::instance(
507 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
508 /**
509 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
510 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
511 *
512 * @author Leo Fajardo (@leorw)
513 * @since 1.2.2
514 */
515 ( is_object( $this->_plugin ) ? $this->_plugin->title : $this->get_plugin_name() ),
516 $this->get_unique_affix()
517 );
518
519 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
520 fs_request_is_action( 'restart_freemius' )
521 ) {
522 FS_Api::clear_cache();
523 $this->_cache->clear();
524 }
525
526 $this->register_constructor_hooks();
527
528 /**
529 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
530 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
531 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
532 *
533 * @author Leo Fajardo (@leorw)
534 */
535 self::migrate_install_plan_to_plan_id( $this->_storage );
536
537 $this->_load_account();
538
539 $this->_version_updates_handler();
540 }
541
542 /**
543 * @author Leo Fajardo (@leorw)
544 * @since 2.3.0
545 */
546 private function maybe_adjust_storage() {
547 $install_timestamp = null;
548 $prev_is_premium = null;
549
550 $options_to_update = array();
551
552 $is_network_admin = fs_is_network_admin();
553
554 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
555
556 if ( ! $is_network_admin ) {
557 if ( is_null( $network_install_timestamp ) ) {
558 // Plugin was not network-activated before.
559 return;
560 }
561
562 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
563 // Set the `install_timestamp` only if it's not yet set.
564 $install_timestamp = $network_install_timestamp;
565 }
566
567 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
568 } else {
569 $current_wp_user = self::_get_current_wp_user();
570 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
571 $network_user_info = array();
572
573 $skips_count = 0;
574
575 $sites = self::get_sites();
576 $sites_count = count( $sites );
577
578 $blog_id_2_install_map = array();
579
580 $is_first_non_ignored_blog = true;
581
582 foreach ( $sites as $site ) {
583 $blog_id = self::get_site_blog_id( $site );
584
585 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
586
587 if ( is_null( $blog_install_timestamp ) ) {
588 // Plugin has not been installed on this blog.
589 continue;
590 }
591
592 $is_earlier_install = (
593 ! is_null( $install_timestamp ) &&
594 $blog_install_timestamp < $install_timestamp
595 );
596
597 $install = $this->get_install_by_blog_id( $blog_id );
598
599 $update_network_user_info = false;
600
601 if ( ! is_object( $install ) ) {
602 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
603 // The opt-in decision (whether to skip or opt in) is yet to be made.
604 continue;
605 }
606
607 $skips_count ++;
608 } else {
609 $blog_id_2_install_map[ $blog_id ] = $install;
610
611 if ( empty( $network_user_info ) ) {
612 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
613 $update_network_user_info = true;
614 }
615
616 if ( ! $update_network_user_info &&
617 is_object( $current_fs_user ) &&
618 $network_user_info['user_id'] != $current_fs_user->id &&
619 $install->user_id == $current_fs_user->id
620 ) {
621 // If an install that is owned by the current WP user is found, use its user information instead.
622 $update_network_user_info = true;
623 }
624
625 if ( ! $update_network_user_info &&
626 $is_earlier_install &&
627 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
628 ) {
629 // 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.
630 $update_network_user_info = true;
631 }
632 }
633
634 if ( $update_network_user_info ) {
635 $network_user_info = array(
636 'user_id' => $install->user_id,
637 'blog_id' => $blog_id
638 );
639 }
640
641 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
642
643 if ( $is_first_non_ignored_blog ) {
644 $prev_is_premium = $site_prev_is_premium;
645
646 if ( is_null( $network_install_timestamp ) ) {
647 $install_timestamp = $blog_install_timestamp;
648 }
649
650 $is_first_non_ignored_blog = false;
651
652 continue;
653 }
654
655 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
656 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
657 $prev_is_premium = null;
658 }
659
660 if ( $is_earlier_install ) {
661 // If an earlier install timestamp is found.
662 $install_timestamp = $blog_install_timestamp;
663 }
664 }
665
666 $installs_count = count( $blog_id_2_install_map );
667
668 if ( $sites_count === ( $installs_count + $skips_count ) ) {
669 if ( ! empty( $network_user_info ) ) {
670 $options_to_update['network_user_id'] = $network_user_info['user_id'];
671 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
672
673 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
674 if ( $install->user_id == $network_user_info['user_id'] ) {
675 continue;
676 }
677
678 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
679 }
680 }
681
682 if ( $sites_count === $skips_count ) {
683 /**
684 * Assume network-level skipping as the intended action if all actions identified were only
685 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
686 */
687 $options_to_update['is_anonymous_ms'] = true;
688 } else if ( $sites_count === $installs_count ) {
689 /**
690 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
691 * (i.e., no delegation and skipping of the connections so far).
692 */
693 $options_to_update['is_network_connected'] = true;
694 }
695 }
696 }
697
698 if ( ! is_null( $install_timestamp ) ) {
699 $options_to_update['install_timestamp'] = $install_timestamp;
700 }
701
702 if ( ! is_null( $prev_is_premium ) ) {
703 $options_to_update['prev_is_premium'] = $prev_is_premium;
704 }
705
706 if ( ! empty( $options_to_update ) ) {
707 $this->adjust_storage( $options_to_update, $is_network_admin );
708 }
709 }
710
711 /**
712 * @author Leo Fajardo (@leorw)
713 * @since 2.3.0
714 *
715 * @param array $options
716 * @param bool $is_network_admin
717 */
718 private function adjust_storage( $options, $is_network_admin ) {
719 foreach ( $options as $name => $value ) {
720 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
721 }
722 }
723
724 /**
725 * Checks whether this module has a settings menu.
726 *
727 * @author Leo Fajardo (@leorw)
728 * @since 1.2.2
729 *
730 * @return bool
731 */
732 function has_settings_menu() {
733 return ( $this->_is_network_active && fs_is_network_admin() ) ?
734 $this->_menu->has_network_menu() :
735 $this->_menu->has_menu();
736 }
737
738 /**
739 * 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.
740 *
741 * @author Vova Feldman (@svovaf)
742 * @since 2.4.5
743 *
744 * @return bool
745 */
746 function show_opt_in_on_themes_page() {
747 if ( ! $this->is_free_wp_org_theme() ) {
748 return false;
749 }
750
751 if ( ! $this->has_settings_menu() ) {
752 return true;
753 }
754
755 return $this->show_settings_with_tabs();
756 }
757
758 /**
759 * If `true` the opt-in should be shown on the product's main setting page.
760 *
761 * @author Vova Feldman (@svovaf)
762 * @since 2.4.5
763 *
764 * @return bool
765 *
766 * @uses show_opt_in_on_themes_page();
767 */
768 function show_opt_in_on_setting_page() {
769 return ! $this->show_opt_in_on_themes_page();
770 }
771
772 /**
773 * If `true` the settings should be shown using tabs.
774 *
775 * @author Vova Feldman (@svovaf)
776 * @since 2.4.5
777 *
778 * @return bool
779 */
780 function show_settings_with_tabs() {
781 return ( self::NAVIGATION_TABS === $this->_navigation );
782 }
783
784 /**
785 * Check if the context module is free wp.org theme.
786 *
787 * This method is helpful because:
788 * 1. wp.org themes are limited to a single submenu item,
789 * and sub-submenu items are most likely not allowed (never verified).
790 * 2. wp.org themes are not allowed to redirect the user
791 * after the theme activation, therefore, the agreed UX
792 * is showing the opt-in as a modal dialog box after
793 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
794 *
795 * @author Vova Feldman (@svovaf)
796 * @since 1.2.2.7
797 *
798 * @return bool
799 */
800 function is_free_wp_org_theme() {
801 return (
802 $this->is_theme() &&
803 $this->is_org_repo_compliant() &&
804 ! $this->is_premium()
805 );
806 }
807
808 /**
809 * Checks whether this a submenu item is visible.
810 *
811 * @author Vova Feldman (@svovaf)
812 * @since 1.2.2.6
813 * @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.
814 *
815 * @param string $slug
816 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
817 *
818 * @return bool
819 */
820 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
821 if ( $this->is_admin_page( $slug ) ) {
822 /**
823 * It is the current context page, so show the submenu item
824 * so the user will have the right context page, even if it
825 * was set to hidden.
826 */
827 return true;
828 }
829
830 if ( ! $this->has_settings_menu() ) {
831 // No menu settings at all.
832 return false;
833 }
834
835 if (
836 ! $is_tabs_visibility_check &&
837 $this->is_org_repo_compliant() &&
838 $this->show_settings_with_tabs()
839 ) {
840 /**
841 * wp.org themes are limited to a single submenu item, and
842 * sub-submenu items are most likely not allowed (never verified).
843 */
844 return false;
845 }
846
847 return $this->_menu->is_submenu_item_visible( $slug );
848 }
849
850 /**
851 * Check if a Freemius page should be accessible via the UI.
852 *
853 * @author Vova Feldman (@svovaf)
854 * @since 1.2.2.7
855 *
856 * @param string $slug
857 *
858 * @return bool
859 */
860 function is_page_visible( $slug ) {
861 if ( $this->is_admin_page( $slug ) ) {
862 return true;
863 }
864
865 return $this->_menu->is_submenu_item_visible( $slug, true, true );
866 }
867
868 /**
869 * @author Vova Feldman (@svovaf)
870 * @since 1.0.9
871 */
872 private function _version_updates_handler() {
873 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
874 // Freemius version upgrade mode.
875 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
876 $this->_storage->sdk_version = $this->version;
877
878 if ( empty( $this->_storage->sdk_last_version ) ||
879 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
880 ) {
881 $this->_storage->sdk_upgrade_mode = true;
882 $this->_storage->sdk_downgrade_mode = false;
883 } else {
884 $this->_storage->sdk_downgrade_mode = true;
885 $this->_storage->sdk_upgrade_mode = false;
886
887 }
888
889 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
890 }
891
892 $plugin_version = $this->get_plugin_version();
893 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
894 // Plugin version upgrade mode.
895 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
896 $this->_storage->plugin_version = $plugin_version;
897
898 if ( empty( $this->_storage->plugin_last_version ) ||
899 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
900 ) {
901 $this->_storage->plugin_upgrade_mode = true;
902 $this->_storage->plugin_downgrade_mode = false;
903 } else {
904 $this->_storage->plugin_downgrade_mode = true;
905 $this->_storage->plugin_upgrade_mode = false;
906 }
907
908 if ( ! empty( $this->_storage->plugin_last_version ) ) {
909 // Different version of the plugin was installed before, therefore it's an update.
910 $this->_storage->is_plugin_new_install = false;
911 }
912
913 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
914 }
915 }
916
917 #--------------------------------------------------------------------------------
918 #region Data Migration on SDK Update
919 #--------------------------------------------------------------------------------
920
921 /**
922 * @author Vova Feldman (@svovaf)
923 * @since 1.1.5
924 *
925 * @param string $sdk_prev_version
926 * @param string $sdk_version
927 */
928 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
929 /**
930 * @since 1.1.7.3 Fixed unwanted connectivity test cleanup.
931 */
932 if ( empty( $sdk_prev_version ) ) {
933 return;
934 }
935
936 if ( version_compare( $sdk_prev_version, '2.1.0', '<' ) &&
937 version_compare( $sdk_version, '2.1.0', '>=' )
938 ) {
939 $this->_storage->handle_gdpr_admin_notice = true;
940 }
941
942 if ( version_compare( $sdk_prev_version, '2.0.0', '<' ) &&
943 version_compare( $sdk_version, '2.0.0', '>=' )
944 ) {
945 $this->migrate_to_subscriptions_collection();
946
947 $this->consolidate_licenses();
948
949 // Clear trial_plan since it's now loaded from the plans collection when needed.
950 $this->_storage->remove( 'trial_plan', true, false );
951 }
952
953 if ( version_compare( $sdk_prev_version, '1.2.3', '<' ) &&
954 version_compare( $sdk_version, '1.2.3', '>=' )
955 ) {
956 /**
957 * Starting from version 1.2.3, paths are stored as relative instead of absolute and some of them can be
958 * invalid.
959 *
960 * @author Leo Fajardo (@leorw)
961 */
962 $this->remove_invalid_paths();
963 }
964
965 if ( version_compare( $sdk_prev_version, '1.1.5', '<' ) &&
966 version_compare( $sdk_version, '1.1.5', '>=' )
967 ) {
968 // On version 1.1.5 merged connectivity and is_on data.
969 if ( isset( $this->_storage->connectivity_test ) ) {
970 if ( ! isset( $this->_storage->is_on ) ) {
971 unset( $this->_storage->connectivity_test );
972 } else {
973 $connectivity_data = $this->_storage->connectivity_test;
974 $connectivity_data['is_active'] = $this->_storage->is_on['is_active'];
975 $connectivity_data['timestamp'] = $this->_storage->is_on['timestamp'];
976
977 // Override.
978 $this->_storage->connectivity_test = $connectivity_data;
979
980 // Remove previous structure.
981 unset( $this->_storage->is_on );
982 }
983
984 }
985 }
986
987 if (
988 version_compare( $sdk_prev_version, '2.2.1', '<' ) &&
989 version_compare( $sdk_version, '2.2.1', '>=' )
990 ) {
991 /**
992 * Clear the file cache without storing the previous path since it could be a wrong path. For example,
993 * 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
994 * as the parent plugin's when the add-on was auto-installed since the relevant method names were not
995 * skipped in the logic that determines the right path in the `get_caller_main_file_and_type` method
996 * (e.g. `try_activate_plugin`). Since it was an auto-installation, the caller was the parent plugin
997 * and so its path was used. In case the stored path is wrong, clearing the cache will resolve issues
998 * related to data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
999 *
1000 * @author Leo Fajardo (@leorw)
1001 * @since 2.2.1
1002 */
1003 $this->clear_module_main_file_cache( false );
1004 }
1005 }
1006
1007 /**
1008 * @author Leo Fajardo (@leorw)
1009 * @since 2.0.0
1010 *
1011 * @param \FS_Storage $storage
1012 * @param bool|int|null $blog_id
1013 */
1014 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
1015 if ( empty( $storage->sdk_version ) ) {
1016 // New installation of the plugin, no need to upgrade.
1017 return;
1018 }
1019
1020 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
1021 // Previous version is >= 2.0.0, so no need to migrate.
1022 return;
1023 }
1024
1025 // Alias.
1026 $module_type = $storage->get_module_type();
1027 $module_slug = $storage->get_module_slug();
1028
1029 $installs = self::get_all_sites( $module_type, $blog_id );
1030 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
1031
1032 if ( ! is_object( $install ) ) {
1033 return;
1034 }
1035
1036 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
1037 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
1038 $install->plan_id = self::_decrypt( $install->plan->id );
1039 }
1040
1041 unset( $install->plan );
1042
1043 $installs[ $module_slug ] = clone $install;
1044
1045 self::set_account_option_by_module(
1046 $module_type,
1047 'sites',
1048 $installs,
1049 true,
1050 $blog_id
1051 );
1052 }
1053 }
1054
1055 /**
1056 * @author Leo Fajardo (@leorw)
1057 * @since 2.0.0
1058 */
1059 private function migrate_to_subscriptions_collection() {
1060 if ( ! is_object( $this->_site ) ) {
1061 return;
1062 }
1063
1064 if ( isset( $this->_storage->subscription ) && is_object( $this->_storage->subscription ) ) {
1065 $this->_storage->subscriptions = array( fs_get_entity( $this->_storage->subscription, FS_Subscription::get_class_name() ) );
1066 }
1067 }
1068
1069 /**
1070 * @author Leo Fajardo (@leorw)
1071 * @since 2.0.0
1072 */
1073 private function consolidate_licenses() {
1074 $plugin_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_PLUGIN );
1075 if ( isset( $plugin_licenses[ $this->_slug ] ) ) {
1076 $plugin_licenses = $plugin_licenses[ $this->_slug ];
1077 } else {
1078 $plugin_licenses = array();
1079 }
1080
1081 $theme_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_THEME );
1082 if ( isset( $theme_licenses[ $this->_slug ] ) ) {
1083 $theme_licenses = $theme_licenses[ $this->_slug ];
1084 } else {
1085 $theme_licenses = array();
1086 }
1087
1088 if ( empty( $plugin_licenses ) && empty( $theme_licenses ) ) {
1089 return;
1090 }
1091
1092 $all_licenses = array();
1093 $user_id_license_ids_map = array();
1094
1095 foreach ( $plugin_licenses as $user_id => $user_licenses ) {
1096 if ( is_array( $user_licenses ) ) {
1097 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
1098 $user_id_license_ids_map[ $user_id ] = array();
1099 }
1100
1101 foreach ( $user_licenses as $user_license ) {
1102 $all_licenses[] = $user_license;
1103 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
1104 }
1105 }
1106 }
1107
1108 foreach ( $theme_licenses as $user_id => $user_licenses ) {
1109 if ( is_array( $user_licenses ) ) {
1110 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
1111 $user_id_license_ids_map[ $user_id ] = array();
1112 }
1113
1114 foreach ( $user_licenses as $user_license ) {
1115 $all_licenses[] = $user_license;
1116 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
1117 }
1118 }
1119 }
1120
1121 self::store_user_id_license_ids_map(
1122 $user_id_license_ids_map,
1123 $this->_module_id
1124 );
1125
1126 $this->_store_licenses( true, $this->_module_id, $all_licenses );
1127 }
1128
1129 /**
1130 * Remove invalid paths.
1131 *
1132 * @author Leo Fajardo (@leorw)
1133 * @since 1.2.3
1134 */
1135 private function remove_invalid_paths() {
1136 // Remove invalid path that is still associated with the current slug if there's any.
1137 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
1138 foreach ( $file_slug_map as $plugin_basename => $slug ) {
1139 if ( $slug === $this->_slug &&
1140 $plugin_basename !== $this->_plugin_basename &&
1141 ! file_exists( $this->get_absolute_path( $plugin_basename ) )
1142 ) {
1143 unset( $file_slug_map[ $plugin_basename ] );
1144 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
1145
1146 break;
1147 }
1148 }
1149 }
1150
1151 /**
1152 * @author Vova Feldman (@svovaf)
1153 * @since 1.2.2.7
1154 *
1155 * @param string $plugin_prev_version
1156 * @param string $plugin_version
1157 */
1158 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1159 if ( $this->is_theme() ) {
1160 // Expire the cache of the previous tabs since the theme may
1161 // have setting updates.
1162 $this->_cache->expire( 'tabs' );
1163 $this->_cache->expire( 'tabs_stylesheets' );
1164 }
1165 }
1166
1167 /**
1168 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1169 * - Moves some data to the network level storage.
1170 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1171 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1172 * - 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.
1173 * - 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.
1174 * - 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.
1175 *
1176 * @author Vova Feldman (@svovaf)
1177 * @since 2.0.0
1178 */
1179 private static function migrate_accounts_to_network() {
1180 $sites = self::get_sites();
1181 $sites_count = count( $sites );
1182 $connection_status = array();
1183 $plugin_slugs = array();
1184 foreach ( $sites as $site ) {
1185 $blog_id = self::get_site_blog_id( $site );
1186
1187 self::$_accounts->migrate_to_network( $blog_id );
1188
1189 /**
1190 * Build a list of all Freemius powered plugins slugs.
1191 */
1192 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1193 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1194 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1195 $plugin_slugs[ $data['slug'] ] = true;
1196 }
1197 }
1198
1199 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1200
1201 if ( is_array( $installs ) ) {
1202 foreach ( $installs as $slug => $install ) {
1203 if ( ! isset( $connection_status[ $slug ] ) ) {
1204 $connection_status[ $slug ] = array();
1205 }
1206
1207 if ( is_object( $install ) &&
1208 FS_Site::is_valid_id( $install->id ) &&
1209 FS_User::is_valid_id( $install->user_id )
1210 ) {
1211 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1212 }
1213 }
1214 }
1215 }
1216
1217 foreach ( $plugin_slugs as $slug => $true ) {
1218 if ( ! isset( $connection_status[ $slug ] ) ) {
1219 $connection_status[ $slug ] = array();
1220 }
1221
1222 foreach ( $sites as $site ) {
1223 $blog_id = self::get_site_blog_id( $site );
1224
1225 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1226 continue;
1227 }
1228
1229 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1230
1231 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1232
1233 if ( ! is_null( $is_anonymous ) ) {
1234 // Since 1.1.3 is_anonymous is an array.
1235 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1236 $is_anonymous = $is_anonymous['is'];
1237 }
1238
1239 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1240 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1241 }
1242 }
1243
1244 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1245 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1246 }
1247 }
1248 }
1249
1250 $super_admins = array();
1251
1252 foreach ( $connection_status as $slug => $blogs_status ) {
1253 $skips = 0;
1254 $ignores = 0;
1255 $connections = 0;
1256 $opted_in_users = array();
1257 $opted_in_super_admins = array();
1258
1259 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1260
1261 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1262 if ( 'skipped' === $status_or_user_id ) {
1263 $skips ++;
1264 } else if ( 'ignored' === $status_or_user_id ) {
1265 $ignores ++;
1266 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1267 $connections ++;
1268
1269 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1270 $opted_in_users[ $status_or_user_id ] = array();
1271 }
1272
1273 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1274
1275 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1276 self::is_super_admin( $status_or_user_id )
1277 ) {
1278 // Cache super-admin data.
1279 $super_admins[ $status_or_user_id ] = true;
1280
1281 // Remember opted-in super-admins for the plugin.
1282 $opted_in_super_admins[ $status_or_user_id ] = true;
1283 }
1284 }
1285 }
1286
1287 $main_super_admin_user_id = null;
1288 $all_migrated = false;
1289 if ( $sites_count == $skips ) {
1290 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1291 $storage->is_anonymous_ms = $storage->is_anonymous;
1292
1293 $all_migrated = true;
1294 } else if ( $sites_count == $ignores ) {
1295 // Don't do anything, still in activation mode.
1296
1297 $all_migrated = true;
1298 } else if ( 0 < count( $opted_in_super_admins ) ) {
1299 // Find the super-admin with the majority of installs.
1300 $max_installs_by_super_admin = 0;
1301 foreach ( $opted_in_super_admins as $user_id => $true ) {
1302 $installs_count = count( $opted_in_users[ $user_id ] );
1303
1304 if ( $installs_count > $max_installs_by_super_admin ) {
1305 $max_installs_by_super_admin = $installs_count;
1306 $main_super_admin_user_id = $user_id;
1307 }
1308 }
1309
1310 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1311 // Super-admin opted-in for all sites in the network.
1312 $storage->is_network_connected = true;
1313
1314 $all_migrated = true;
1315 }
1316
1317 // Store network user.
1318 $storage->network_user_id = $main_super_admin_user_id;
1319
1320 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1321 // Since all sites are opted-in, associating with the main site.
1322 get_current_blog_id() :
1323 // Associating with the 1st found opted-in site.
1324 $opted_in_users[ $main_super_admin_user_id ][0];
1325
1326 /**
1327 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1328 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1329 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1330 * will be empty.
1331 */
1332 $storage->migrate_to_network();
1333 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1334 } else {
1335 // At least one opt-in. All the opt-in were created by a non-super-admin.
1336 if ( 0 == $ignores ) {
1337 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1338 $storage->store( 'is_delegated_connection', true, true );
1339
1340 $all_migrated = true;
1341 }
1342 }
1343
1344 if ( ! $all_migrated ) {
1345 /**
1346 * Delegate all sites that were:
1347 * 1) Opted-in by a user that is NOT the main-super-admin.
1348 * 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.
1349 */
1350 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1351 if ( $status_or_user_id == $main_super_admin_user_id ) {
1352 continue;
1353 }
1354
1355 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1356 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1357 ) {
1358 $storage->store( 'is_delegated_connection', true, $blog_id );
1359 }
1360 }
1361 }
1362
1363
1364 if ( ( $connections + $skips > 0 ) ) {
1365 if ( $ignores > 0 ) {
1366 /**
1367 * If admin already opted-in or skipped in any of the network sites, and also
1368 * have sites which the connection decision was not yet taken, set this plugin
1369 * into network activation mode so the super-admin can choose what to do with
1370 * the rest of the sites.
1371 */
1372 self::set_network_upgrade_mode( $storage );
1373 }
1374 }
1375 }
1376 }
1377
1378 /**
1379 * Set a module into network upgrade mode.
1380 *
1381 * @author Vova Feldman (@svovaf)
1382 * @since 2.0.0
1383 *
1384 * @param \FS_Storage $storage
1385 *
1386 * @return bool
1387 */
1388 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1389 return $storage->is_network_activation = true;
1390 }
1391
1392 /**
1393 * Will return true after upgrading to the SDK with the network level integration,
1394 * when the super-admin involvement is required regarding the rest of the sites.
1395 *
1396 * @author Vova Feldman (@svovaf)
1397 * @since 2.0.0
1398 *
1399 * @return bool
1400 */
1401 function is_network_upgrade_mode() {
1402 return $this->_storage->get( 'is_network_activation' );
1403 }
1404
1405 /**
1406 * Clear flag after the upgrade mode completion.
1407 *
1408 * @author Vova Feldman (@svovaf)
1409 * @since 2.0.0
1410 *
1411 * @return bool True if network activation was on and now completed.
1412 */
1413 private function network_upgrade_mode_completed() {
1414 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1415 $this->_storage->remove( 'is_network_activation' );
1416
1417 return true;
1418 }
1419
1420 return false;
1421 }
1422
1423 #endregion
1424
1425 /**
1426 * This action is connected to the 'plugins_loaded' hook and helps to determine
1427 * if this is a new plugin installation or a plugin update.
1428 *
1429 * There are 3 different use-cases:
1430 * 1) New plugin installation right with Freemius:
1431 * 1.1 _activate_plugin_event_hook() will be executed first
1432 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1433 * and $this->_storage->plugin_last_version is not set,
1434 * $this->_storage->is_plugin_new_install will be set to TRUE.
1435 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1436 * be already set to TRUE.
1437 *
1438 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1439 * 2.1 _activate_plugin_event_hook() will not be executed, because
1440 * the activation hook do NOT fires on updates since WP 3.1.
1441 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1442 * be empty, therefore, it will be set to FALSE.
1443 *
1444 * 3) Plugin update, had Freemius in prev version as well:
1445 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1446 * before, $this->_storage->plugin_last_version will NOT be empty,
1447 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1448 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1449 * already set, therefore, it will not be modified.
1450 *
1451 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1452 *
1453 * NOTE:
1454 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1455 * and then, the next immediate PageView is the plugin's main settings page, it will not
1456 * show the opt-in right away. The reason it will happen is because Freemius execution
1457 * will be turned off till the plugin is fully loaded at least once
1458 * (till $this->_storage->was_plugin_loaded is TRUE).
1459 *
1460 * @author Vova Feldman (@svovaf)
1461 * @since 1.1.9
1462 *
1463 */
1464 function _plugins_loaded() {
1465 // Update flag that plugin was loaded with Freemius at least once.
1466 $this->_storage->was_plugin_loaded = true;
1467
1468 /**
1469 * Bug fix - only set to false when it's a plugin, due to the
1470 * execution sequence of the theme hooks and our methods, if
1471 * this will be set for themes, Freemius will always assume
1472 * it's a theme update.
1473 *
1474 * @author Vova Feldman (@svovaf)
1475 * @since 1.2.2.2
1476 */
1477 if ( $this->is_plugin() &&
1478 ! isset( $this->_storage->is_plugin_new_install )
1479 ) {
1480 $this->_storage->is_plugin_new_install = (
1481 ! is_plugin_active( $this->_plugin_basename ) &&
1482 empty( $this->_storage->plugin_last_version )
1483 );
1484 }
1485 }
1486
1487 /**
1488 * Add special parameter to WP admin AJAX calls so when we
1489 * process AJAX calls we can identify its source properly.
1490 *
1491 * @author Leo Fajardo (@leorw)
1492 * @since 2.0.0
1493 */
1494 static function _enrich_ajax_url() {
1495 $admin_param = is_network_admin() ?
1496 '_fs_network_admin' :
1497 '_fs_blog_admin';
1498 ?>
1499 <script type="text/javascript">
1500 (function ($) {
1501 $(document).ajaxSend(function (event, jqxhr, settings) {
1502 if (settings.url &&
1503 -1 < settings.url.indexOf('admin-ajax.php') &&
1504 ! ( settings.url.indexOf( '<?php echo $admin_param ?>' ) > 0 )
1505 ) {
1506 if (
1507 'string' === typeof settings.data &&
1508 settings.data.indexOf( 'action=heartbeat' ) > 0
1509 ) {
1510 return;
1511 }
1512
1513 if (settings.url.indexOf('?') > 0) {
1514 settings.url += '&';
1515 } else {
1516 settings.url += '?';
1517 }
1518
1519 settings.url += '<?php echo $admin_param ?>=true';
1520 }
1521 });
1522 })(jQuery);
1523 </script>
1524 <?php
1525 }
1526
1527 /**
1528 * Opens the support forum subemenu item in a new browser page.
1529 *
1530 * @author Vova Feldman (@svovaf)
1531 * @since 2.1.4
1532 */
1533 static function _open_support_forum_in_new_page() {
1534 ?>
1535 <script type="text/javascript">
1536 (function ($) {
1537 $('.fs-submenu-item.wp-support-forum').parent().attr( { target: '_blank', rel: 'noopener noreferrer' } );
1538 })(jQuery);
1539 </script>
1540 <?php
1541 }
1542
1543 /**
1544 * @author Vova Feldman (@svovaf)
1545 * @since 1.0.9
1546 */
1547 private function register_constructor_hooks() {
1548 $this->_logger->entrance();
1549
1550 if ( is_admin() ) {
1551 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1552
1553 if ( $this->is_plugin() ) {
1554 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1555 /**
1556 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1557 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1558 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1559 * updating of a .org plugin).
1560 */
1561 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1562 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1563 /**
1564 * 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.
1565 *
1566 * @author Leo Fajardo (@leorw)
1567 * @since 2.2.3
1568 */
1569 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1570 }
1571
1572 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1573
1574 /**
1575 * @since 1.2.2
1576 *
1577 * Hook to both free and premium version activations to support
1578 * auto deactivation on the other version activation.
1579 */
1580 register_activation_hook(
1581 $plugin_dir . $this->_free_plugin_basename,
1582 array( &$this, '_activate_plugin_event_hook' )
1583 );
1584
1585 register_activation_hook(
1586 $plugin_dir . $this->premium_plugin_basename(),
1587 array( &$this, '_activate_plugin_event_hook' )
1588 );
1589 } else {
1590 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1591
1592 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1593 }
1594
1595 /**
1596 * Part of the mechanism to identify new plugin install vs. plugin update.
1597 *
1598 * @author Vova Feldman (@svovaf)
1599 * @since 1.1.9
1600 */
1601 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1602 /**
1603 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1604 * when the logic gets here since the activation logic first add the activate plugins,
1605 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1606 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1607 * plugin activation, and that IS intentional.
1608 *
1609 * @author Vova Feldman (@svovaf)
1610 */
1611 if ( $this->is_plugin() &&
1612 $this->is_activation_mode( false ) &&
1613 0 == did_action( 'plugins_loaded' )
1614 ) {
1615 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1616 } else {
1617 // If was activated before, then it was already loaded before.
1618 $this->_plugins_loaded();
1619 }
1620 }
1621
1622 if ( ! self::is_ajax() ) {
1623 if ( ! $this->is_addon() ) {
1624 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1625 }
1626 }
1627
1628 if ( $this->_storage->handle_gdpr_admin_notice ) {
1629 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1630 }
1631
1632 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1633 add_action( 'init', array( &$this, '_maybe_add_pricing_ajax_handler' ) );
1634 }
1635
1636 if ( $this->is_plugin() ) {
1637 if ( $this->_is_network_active ) {
1638 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1639 }
1640
1641 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1642 }
1643
1644 if ( is_multisite() ) {
1645 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1646 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1647 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1648 add_action( 'deleted_blog', array( &$this, '_after_site_deleted_callback' ), 10, 2 );
1649
1650 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1651 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1652 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1653 }
1654
1655 if ( $this->is_theme() &&
1656 self::is_customizer() &&
1657 $this->apply_filters( 'show_customizer_upsell', true )
1658 ) {
1659 // Register customizer upsell.
1660 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1661 }
1662
1663 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1664
1665 if ( $this->is_theme() && ! $this->is_migration() ) {
1666 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1667 }
1668
1669 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1670 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1671 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1672 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1673
1674 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1675 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1676 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1677 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1678
1679 if ( $this->_is_network_active && fs_is_network_admin() ) {
1680 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1681 }
1682
1683 $this->add_ajax_action( 'install_premium_version', array(
1684 &$this,
1685 '_install_premium_version_ajax_action'
1686 ) );
1687
1688 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1689
1690 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1691
1692 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1693
1694 $this->add_action(
1695 'plugin_version_update',
1696 array( &$this, '_after_version_update' ),
1697 WP_FS__DEFAULT_PRIORITY,
1698 2
1699 );
1700 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1701
1702 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
1703 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
1704 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1705
1706 /**
1707 * Handle request to reset anonymous mode for `get_reconnect_url()`.
1708 *
1709 * @author Vova Feldman (@svovaf)
1710 * @since 1.2.1.5
1711 */
1712 if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
1713 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
1714 ) {
1715 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1716 }
1717 }
1718
1719 /**
1720 * Register the required hooks right after the settings parse is completed.
1721 *
1722 * @author Vova Feldman (@svovaf)
1723 * @since 2.3.1
1724 */
1725 private function register_after_settings_parse_hooks() {
1726 if ( is_admin() &&
1727 $this->is_theme() &&
1728 $this->is_premium() &&
1729 ! $this->has_active_valid_license()
1730 ) {
1731 $this->add_ajax_action(
1732 'delete_theme_update_data',
1733 array( &$this, '_delete_theme_update_data_action' )
1734 );
1735 }
1736
1737 if ( $this->show_settings_with_tabs() ) {
1738 /**
1739 * Include the required hooks to capture the theme settings' page tabs
1740 * and cache them.
1741 *
1742 * @author Vova Feldman (@svovaf)
1743 * @since 1.2.2.7
1744 */
1745 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1746 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1747 // Add license activation AJAX callback.
1748 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1749
1750 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1751 }
1752
1753 add_action(
1754 'admin_footer',
1755 array( &$this, '_add_freemius_tabs' ),
1756 /**
1757 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1758 * That's why the priority is 11 while the tabs capture logic is added
1759 * with priority 10.
1760 *
1761 * @author Vova Feldman (@svovaf)
1762 */
1763 11
1764 );
1765 }
1766
1767 if ( ! self::is_ajax() ) {
1768 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1769 add_action(
1770 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1771 array( &$this, '_prepare_admin_menu' ),
1772 WP_FS__LOWEST_PRIORITY
1773 );
1774 }
1775 }
1776 }
1777
1778 /**
1779 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1780 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1781 *
1782 * @author Leo Fajardo (@leorw)
1783 * @since 2.2.3
1784 *
1785 * @param object $updates
1786 * @param string|null $transient
1787 *
1788 * @return object
1789 */
1790 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1791 if ( is_object( $updates ) && isset( $updates->response ) ) {
1792 foreach ( $updates->response as $file => $plugin ) {
1793 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1794 unset( $updates->response[ $file ] );
1795 }
1796 }
1797 }
1798
1799 return $updates;
1800 }
1801
1802 /**
1803 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1804 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1805 *
1806 * @author Leo Fajardo (@leorw)
1807 * @since 2.2.3
1808 *
1809 * @return string
1810 */
1811 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1812 $slug_basename_map = array();
1813 foreach ( self::$_instances as $instance ) {
1814 if ( ! $instance->is_plugin() ) {
1815 continue;
1816 }
1817
1818 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1819 }
1820 ?>
1821 <script type="text/javascript">
1822 (function( $ ) {
1823 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1824 for ( var slug in slugBasenameMap ) {
1825 var basename = slugBasenameMap[ slug ];
1826
1827 // Try to get the plugin rows if on the "Plugins" page.
1828 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1829
1830 if ( 0 === $pluginRows.length ) {
1831 // Try to get the plugin rows if on the "Updates" page.
1832 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1833 if ( 0 !== $pluginCheckbox.length ) {
1834 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1835 }
1836 }
1837
1838 if ( 0 === $pluginRows.length ) {
1839 // No plugin rows found.
1840 continue;
1841 }
1842
1843 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1844 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1845 var $this = $( this ),
1846 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1847
1848 $this.attr( 'href', href );
1849 });
1850 }
1851 })( jQuery );
1852 </script>
1853 <?php
1854 }
1855
1856 /**
1857 * @author Leo Fajardo (@leorw)
1858 * @since 2.3.0
1859 */
1860 static function _maybe_add_beta_label_styles() {
1861 $has_any_beta_version = false;
1862
1863 foreach ( self::$_instances as $instance ) {
1864 if ( $instance->is_beta() ) {
1865 $has_any_beta_version = true;
1866 break;
1867 }
1868 }
1869
1870 if ( $has_any_beta_version ) {
1871 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1872 }
1873 }
1874
1875 /**
1876 * @author Leo Fajardo (@leorw)
1877 * @since 2.3.0
1878 */
1879 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1880 $beta_data = array();
1881
1882 foreach ( self::$_instances as $instance ) {
1883 if ( ! $instance->is_premium() ) {
1884 continue;
1885 }
1886
1887 /**
1888 * If there's an available beta version update, a confirmation message will be shown when the
1889 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1890 */
1891 $has_beta_update = $instance->has_beta_update();
1892
1893 $is_beta = (
1894 // The "Beta" label is added separately for themes.
1895 $instance->is_plugin() &&
1896 $instance->is_beta()
1897 );
1898
1899 if ( ! $is_beta && ! $has_beta_update ) {
1900 continue;
1901 }
1902
1903 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1904
1905 if ( ! $has_beta_update ) {
1906 continue;
1907 }
1908
1909 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1910 '%s %s',
1911 sprintf(
1912 fs_esc_attr_inline(
1913 '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.',
1914 'beta-version-update-caution',
1915 $instance->get_slug()
1916 ),
1917 $instance->get_plugin_title()
1918 ),
1919 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1920 );
1921 }
1922
1923 if ( empty( $beta_data ) ) {
1924 return;
1925 }
1926 ?>
1927 <script type="text/javascript">
1928 ( function( $ ) {
1929 var betaData = <?php echo json_encode( $beta_data ) ?>;
1930
1931 for ( var pluginBasename in betaData ) {
1932 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1933 continue;
1934 }
1935
1936 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1937 continue;
1938 }
1939
1940 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1941 if ( 0 === $parentContainer.length ) {
1942 continue;
1943 }
1944
1945 $parentContainer.find( '.plugin-title > strong:first-child').append(
1946 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1947 );
1948 }
1949
1950 setTimeout( function() {
1951 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1952 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1953 var $parentContainer = $( this ).parents( 'tr:first' );
1954 pluginBasename = ( 0 !== $parentContainer.length ) ?
1955 $parentContainer.data( 'plugin' ) :
1956 $( this ).parents( '.theme:first' ).data( 'slug' );
1957
1958 if (
1959 betaData[ pluginBasename ] &&
1960 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1961 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1962 ) {
1963 return false;
1964 }
1965 } );
1966 }, 20 );
1967 } )( jQuery );
1968 </script>
1969 <?php
1970 }
1971
1972 /**
1973 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1974 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1975 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1976 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1977 *
1978 * @author Leo Fajardo (@leorw)
1979 *
1980 * @since 1.2.0
1981 */
1982 private function unregister_uninstall_hook() {
1983 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1984 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1985 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1986
1987 update_option( 'uninstall_plugins', $uninstallable_plugins );
1988 }
1989
1990 /**
1991 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1992 *
1993 * @param bool $store_prev_path
1994 */
1995 private function clear_module_main_file_cache( $store_prev_path = true ) {
1996 if ( ! isset( $this->_storage->plugin_main_file ) ||
1997 empty( $this->_storage->plugin_main_file->path )
1998 ) {
1999 return;
2000 }
2001
2002 if ( ! $store_prev_path ) {
2003 /**
2004 * Storing the previous path is not needed when clearing the cache after an SDK version update since
2005 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
2006 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
2007 *
2008 * @author Leo Fajardo (@leorw)
2009 * @since 2.2.1
2010 */
2011 unset( $this->_storage->plugin_main_file->path );
2012 } else {
2013 $plugin_main_file = clone $this->_storage->plugin_main_file;
2014
2015 // Store cached path (2nd layer cache).
2016 $plugin_main_file->prev_path = $plugin_main_file->path;
2017
2018 // Clear cached path.
2019 unset( $plugin_main_file->path );
2020
2021 $this->_storage->plugin_main_file = $plugin_main_file;
2022 }
2023
2024 /**
2025 * Clear global cached path.
2026 *
2027 * @author Leo Fajardo (@leorw)
2028 * @since 1.2.2
2029 */
2030 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
2031 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
2032 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2033 }
2034
2035 /**
2036 * @author Leo Fajardo (@leorw)
2037 * @since 2.0.0
2038 */
2039 function _hook_action_links_and_register_account_hooks() {
2040 if ( $this->is_migration() ) {
2041 return;
2042 }
2043
2044 $this->_add_tracking_links();
2045
2046 if ( self::is_plugins_page() && $this->is_plugin() ) {
2047 $this->hook_plugin_action_links();
2048 }
2049
2050 $this->_register_account_hooks();
2051 }
2052
2053 /**
2054 * @author Vova Feldman (@svovaf)
2055 * @since 1.0.9
2056 */
2057 private function _register_account_hooks() {
2058 if ( ! is_admin() ) {
2059 return;
2060 }
2061
2062 /**
2063 * Always show the deactivation feedback form since we added
2064 * automatic free version deactivation upon premium code activation.
2065 *
2066 * @since 1.2.1.6
2067 */
2068 $this->add_ajax_action(
2069 'submit_uninstall_reason',
2070 array( &$this, '_submit_uninstall_reason_action' )
2071 );
2072
2073 $this->add_ajax_action(
2074 'cancel_subscription_or_trial',
2075 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
2076 );
2077
2078 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
2079 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
2080 ( $this->is_theme() && self::is_themes_page() )
2081 ) {
2082 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
2083 }
2084 }
2085 }
2086
2087 /**
2088 * Leverage backtrace to find caller plugin file path.
2089 *
2090 * @author Vova Feldman (@svovaf)
2091 * @since 1.0.6
2092 *
2093 * @param bool $is_init Is initiation sequence.
2094 *
2095 * @return string
2096 */
2097 private function _find_caller_plugin_file( $is_init = false ) {
2098 // Try to load the cached value of the file path.
2099 if ( isset( $this->_storage->plugin_main_file ) ) {
2100 $plugin_main_file = $this->_storage->plugin_main_file;
2101 if ( ! empty( $plugin_main_file->path ) ) {
2102 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
2103 if ( file_exists( $absolute_path ) ) {
2104 return $absolute_path;
2105 }
2106 }
2107 }
2108
2109 /**
2110 * @since 1.2.1
2111 *
2112 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
2113 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
2114 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
2115 *
2116 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
2117 * when the class instantiator isn't the module.
2118 */
2119 if ( ! $is_init ) {
2120 // Fetch prev path cache.
2121 if ( isset( $this->_storage->plugin_main_file ) &&
2122 ! empty( $this->_storage->plugin_main_file->prev_path )
2123 ) {
2124 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2125 if ( file_exists( $absolute_path ) ) {
2126 return $absolute_path;
2127 }
2128 }
2129
2130 wp_die(
2131 $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' ) .
2132 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2133 $this->get_text_inline( 'Error', 'error' ),
2134 array( 'back_link' => true )
2135 );
2136 }
2137
2138 /**
2139 * @since 1.2.1
2140 *
2141 * Only the original instantiator that calls dynamic_init can modify the module's path.
2142 */
2143 // Find caller module.
2144 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2145 $this->_storage->plugin_main_file = (object) array(
2146 'path' => $id_slug_type_path_map[ $this->_module_id ]['path'],
2147 );
2148
2149 return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
2150 }
2151
2152 /**
2153 * @author Leo Fajardo (@leorw)
2154 * @since 1.2.3
2155 *
2156 * @param string $path
2157 *
2158 * @return string
2159 */
2160 private function get_relative_path( $path ) {
2161 $module_root_dir = $this->get_module_root_dir_path();
2162 if ( 0 === strpos( $path, $module_root_dir ) ) {
2163 $path = substr( $path, strlen( $module_root_dir ) );
2164 }
2165
2166 return $path;
2167 }
2168
2169 /**
2170 * @author Leo Fajardo (@leorw)
2171 * @since 1.2.3
2172 *
2173 * @param string $path
2174 * @param string|bool $module_type
2175 *
2176 * @return string
2177 */
2178 private function get_absolute_path( $path, $module_type = false ) {
2179 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2180 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2181 $path = fs_normalize_path( $module_root_dir . $path );
2182 }
2183
2184 return $path;
2185 }
2186
2187 /**
2188 * @author Leo Fajardo (@leorw)
2189 * @since 1.2.3
2190 *
2191 * @param string|bool $module_type
2192 *
2193 * @return string
2194 */
2195 private function get_module_root_dir_path( $module_type = false ) {
2196 $is_plugin = empty( $module_type ) ?
2197 $this->is_plugin() :
2198 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2199
2200 return fs_normalize_path( trailingslashit( $is_plugin ?
2201 WP_PLUGIN_DIR :
2202 get_theme_root( get_stylesheet() ) ) );
2203 }
2204
2205 /**
2206 * @author Leo Fajardo (@leorw)
2207 *
2208 * @param number $module_id
2209 * @param string $slug
2210 *
2211 * @since 1.2.2
2212 */
2213 private function store_id_slug_type_path_map( $module_id, $slug ) {
2214 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2215
2216 $store_option = false;
2217
2218 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2219 $id_slug_type_path_map[ $module_id ] = array(
2220 'slug' => $slug
2221 );
2222
2223 $store_option = true;
2224 } else if (
2225 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2226 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2227 ) {
2228 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2229 $store_option = true;
2230 }
2231
2232 if ( empty( $id_slug_type_path_map[ $module_id ]['path'] ) ||
2233 /**
2234 * This verification is for cases when suddenly the same module
2235 * is installed but with a different folder name.
2236 *
2237 * @author Vova Feldman (@svovaf)
2238 * @since 1.2.3
2239 */
2240 ! file_exists( $this->get_absolute_path(
2241 $id_slug_type_path_map[ $module_id ]['path'],
2242 $id_slug_type_path_map[ $module_id ]['type']
2243 ) )
2244 ) {
2245 $caller_main_file_and_type = $this->get_caller_main_file_and_type();
2246
2247 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2248 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2249
2250 $store_option = true;
2251 }
2252
2253 if ( $store_option ) {
2254 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2255 }
2256 }
2257
2258 /**
2259 * Identifies the caller type: plugin or theme.
2260 *
2261 * @author Leo Fajardo (@leorw)
2262 * @since 1.2.2
2263 *
2264 * @author Vova Feldman (@svovaf)
2265 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2266 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2267 * SDK an internal file instead of directly from functions.php.
2268 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2269 */
2270 private function get_caller_main_file_and_type() {
2271 self::require_plugin_essentials();
2272
2273 $all_plugins = fs_get_plugins( true );
2274 $all_plugins_paths = array();
2275
2276 // Get active plugin's main files real full names (might be symlinks).
2277 foreach ( $all_plugins as $relative_path => $data ) {
2278 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2279 /**
2280 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2281 * can't really include the SDK.
2282 *
2283 * @author Vova Feldman
2284 * @since 1.2.1.7
2285 */
2286 continue;
2287 }
2288
2289 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2290 }
2291
2292 $caller_file_candidate = false;
2293 $caller_map = array();
2294 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2295 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2296 $plugin_dir_to_skip = false;
2297
2298 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2299 if ( empty( $bt[ $i ]['file'] ) ) {
2300 continue;
2301 }
2302
2303 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2304 // If file same as the prev file in the stack, skip it.
2305 continue;
2306 }
2307
2308 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2309 'do_action',
2310 'apply_filter',
2311 // The string split is stupid, but otherwise, theme check
2312 // throws info notices.
2313 'requir' . 'e_once',
2314 'requir' . 'e',
2315 'includ' . 'e_once',
2316 'includ' . 'e',
2317 'install_and_activate_plugin',
2318 'try_activate_plugin',
2319 'activate_plugin'
2320 ) )
2321 ) {
2322 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2323 /**
2324 * Store the directory of the activator plugin so that any other file that starts with it
2325 * cannot be mistakenly chosen as a candidate caller file.
2326 *
2327 * @author Leo Fajardo
2328 *
2329 * @since 2.3.0
2330 */
2331 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2332
2333 foreach ( $all_plugins_paths as $plugin_path ) {
2334 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2335 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2336 $plugin_dir_to_skip = $plugin_dir;
2337
2338 break;
2339 }
2340 }
2341 }
2342
2343 // Ignore call stack hooks and files inclusion.
2344 continue;
2345 }
2346
2347 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2348
2349 if ( ! empty( $plugin_dir_to_skip ) ) {
2350 /**
2351 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2352 *
2353 * @author Leo Fajardo
2354 *
2355 * @since 2.3.0
2356 */
2357 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2358 continue;
2359 }
2360 }
2361
2362 if ( 'functions.php' === basename( $caller_file_path ) ) {
2363 /**
2364 * 1. Assumes that theme's starting execution file is functions.php.
2365 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2366 *
2367 * @author Vova Feldman (@svovaf)
2368 * @since 1.2.2.5
2369 */
2370
2371 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2372 $module_type = WP_FS__MODULE_TYPE_THEME;
2373
2374 /**
2375 * Relative path of the theme, e.g.:
2376 * `my-theme/functions.php`
2377 *
2378 * @author Leo Fajardo (@leorw)
2379 */
2380 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2381 '/' .
2382 basename( $caller_file_path );
2383
2384 continue;
2385 }
2386 }
2387
2388 $caller_file_hash = md5( $caller_file_path );
2389
2390 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2391 foreach ( $all_plugins_paths as $plugin_path ) {
2392 if ( empty( $plugin_path ) ) {
2393 continue;
2394 }
2395
2396 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2397 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2398 break;
2399 }
2400 }
2401 }
2402
2403 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2404 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2405 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2406 }
2407 }
2408
2409 return (object) array(
2410 'module_type' => $module_type,
2411 'path' => $caller_file_candidate
2412 );
2413 }
2414
2415 #----------------------------------------------------------------------------------
2416 #region Deactivation Feedback Form
2417 #----------------------------------------------------------------------------------
2418
2419 /**
2420 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2421 * page.
2422 *
2423 * @author Vova Feldman (@svovaf)
2424 * @author Leo Fajardo (@leorw)
2425 *
2426 * @since 1.1.2
2427 */
2428 function _add_deactivation_feedback_dialog_box() {
2429 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2430 $this->_get_subscription_cancellation_dialog_box_template_params() :
2431 array();
2432
2433 /**
2434 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2435 */
2436 $show_deactivation_feedback_form = true;
2437 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2438 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2439 } else if ( $this->is_addon() ) {
2440 /**
2441 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2442 */
2443 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2444 }
2445
2446 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2447
2448 if (
2449 empty( $subscription_cancellation_dialog_box_template_params ) &&
2450 ! $show_deactivation_feedback_form &&
2451 empty( $uninstall_confirmation_message )
2452 ) {
2453 return;
2454 }
2455
2456 $vars = array( 'id' => $this->_module_id );
2457
2458 if ( $show_deactivation_feedback_form ) {
2459 /* Check the type of user:
2460 * 1. Long-term (long-term)
2461 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2462 * 3. Short-term (short-term)
2463 */
2464 $is_long_term_user = true;
2465
2466 // Check if the site is at least 2 days old.
2467 $time_installed = $this->_storage->install_timestamp;
2468
2469 // Difference in seconds.
2470 $date_diff = time() - $time_installed;
2471
2472 // Convert seconds to days.
2473 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2474
2475 if ( $date_diff_days < 2 ) {
2476 $is_long_term_user = false;
2477 }
2478
2479 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2480
2481 if ( $is_long_term_user ) {
2482 $user_type = 'long-term';
2483 } else {
2484 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2485 $user_type = 'non-registered-and-non-anonymous-short-term';
2486 } else {
2487 $user_type = 'short-term';
2488 }
2489 }
2490
2491 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2492
2493 $vars['reasons'] = $uninstall_reasons;
2494 }
2495
2496 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2497 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2498 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2499
2500 /**
2501 * Load the HTML template for the deactivation feedback dialog box.
2502 *
2503 * @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.
2504 */
2505 fs_require_template( 'forms/deactivation/form.php', $vars );
2506 }
2507
2508 /**
2509 * @author Leo Fajardo (@leorw)
2510 * @since 1.1.2
2511 *
2512 * @param string $user_type
2513 *
2514 * @return array The uninstall reasons for the specified user type.
2515 */
2516 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2517 $module_type = $this->_module_type;
2518
2519 $internal_message_template_var = array(
2520 'id' => $this->_module_id
2521 );
2522
2523 $plan = $this->get_plan();
2524
2525 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2526 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2527 } else {
2528 $contact_support_template = '';
2529 }
2530
2531 $reason_found_better_plugin = array(
2532 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2533 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2534 'input_type' => 'textfield',
2535 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2536 );
2537
2538 $reason_temporary_deactivation = array(
2539 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2540 'text' => sprintf(
2541 $this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ),
2542 strtolower( $this->is_plugin() ?
2543 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2544 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2545 )
2546 ),
2547 'input_type' => '',
2548 'input_placeholder' => ''
2549 );
2550
2551 $reason_other = array(
2552 'id' => self::REASON_OTHER,
2553 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2554 'input_type' => 'textfield',
2555 'input_placeholder' => ''
2556 );
2557
2558 $long_term_user_reasons = array(
2559 array(
2560 'id' => self::REASON_NO_LONGER_NEEDED,
2561 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2562 'input_type' => '',
2563 'input_placeholder' => ''
2564 ),
2565 $reason_found_better_plugin,
2566 array(
2567 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2568 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2569 'input_type' => '',
2570 'input_placeholder' => ''
2571 ),
2572 array(
2573 'id' => self::REASON_BROKE_MY_SITE,
2574 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2575 'input_type' => '',
2576 'input_placeholder' => '',
2577 'internal_message' => $contact_support_template
2578 ),
2579 array(
2580 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2581 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2582 'input_type' => '',
2583 'input_placeholder' => '',
2584 'internal_message' => $contact_support_template
2585 )
2586 );
2587
2588 if ( $this->is_paying() ) {
2589 $long_term_user_reasons[] = array(
2590 'id' => self::REASON_CANT_PAY_ANYMORE,
2591 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2592 'input_type' => 'textfield',
2593 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2594 );
2595 }
2596
2597 $reason_dont_share_info = array(
2598 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2599 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2600 'input_type' => '',
2601 'input_placeholder' => ''
2602 );
2603
2604 /**
2605 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2606 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2607 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2608 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2609 * button in the opt-in form is shown/hidden).
2610 */
2611 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2612 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2613 }
2614
2615 $uninstall_reasons = array(
2616 'long-term' => $long_term_user_reasons,
2617 'non-registered-and-non-anonymous-short-term' => array(
2618 array(
2619 'id' => self::REASON_DIDNT_WORK,
2620 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2621 'input_type' => '',
2622 'input_placeholder' => ''
2623 ),
2624 $reason_dont_share_info,
2625 $reason_found_better_plugin
2626 ),
2627 'short-term' => array(
2628 array(
2629 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2630 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2631 'input_type' => '',
2632 'input_placeholder' => '',
2633 'internal_message' => $contact_support_template
2634 ),
2635 $reason_found_better_plugin,
2636 array(
2637 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2638 '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 ),
2639 'input_type' => 'textarea',
2640 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2641 ),
2642 array(
2643 'id' => self::REASON_NOT_WORKING,
2644 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2645 'input_type' => 'textarea',
2646 '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' )
2647 ),
2648 array(
2649 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2650 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2651 'input_type' => 'textarea',
2652 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2653 ),
2654 array(
2655 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2656 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2657 'input_type' => 'textarea',
2658 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2659 )
2660 )
2661 );
2662
2663 // Randomize the reasons for the current user type.
2664 shuffle( $uninstall_reasons[ $user_type ] );
2665
2666 // Keep the following reasons as the last items in the list.
2667 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2668 $uninstall_reasons[ $user_type ][] = $reason_other;
2669
2670 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2671
2672 return $uninstall_reasons[ $user_type ];
2673 }
2674
2675 /**
2676 * Called after the user has submitted his reason for deactivating the plugin.
2677 *
2678 * @author Leo Fajardo (@leorw)
2679 * @since 1.1.2
2680 */
2681 function _submit_uninstall_reason_action() {
2682 $this->_logger->entrance();
2683
2684 $this->check_ajax_referer( 'submit_uninstall_reason' );
2685
2686 $reason_id = fs_request_get( 'reason_id' );
2687
2688 // Check if the given reason ID is an unsigned integer.
2689 if ( ! ctype_digit( $reason_id ) ) {
2690 exit;
2691 }
2692
2693 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2694 if ( ! empty( $reason_info ) ) {
2695 $reason_info = substr( $reason_info, 0, 128 );
2696 }
2697
2698 $reason = (object) array(
2699 'id' => $reason_id,
2700 'info' => $reason_info,
2701 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2702 );
2703
2704 $this->_storage->store( 'uninstall_reason', $reason );
2705
2706 /**
2707 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2708 * not support uninstall hook.
2709 *
2710 * @author Leo Fajardo (@leorw)
2711 * @since 1.2.2
2712 */
2713 if ( $this->is_theme() ) {
2714 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2715 FS_Plugin_Updater::instance( $this )->delete_update_data();
2716 }
2717
2718 $this->_uninstall_plugin_event( false );
2719 $this->remove_sdk_reference();
2720 }
2721
2722 // Print '1' for successful operation.
2723 echo 1;
2724 exit;
2725 }
2726
2727 /**
2728 * @author Leo Fajardo (@leorw)
2729 * @since 2.1.4
2730 */
2731 function cancel_subscription_or_trial_ajax_action() {
2732 $this->_logger->entrance();
2733
2734 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2735
2736 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2737
2738 if ( $this->is_api_error( $result ) ) {
2739 $this->shoot_ajax_failure( $result->error->message );
2740 }
2741
2742 $this->shoot_ajax_success();
2743 }
2744
2745 /**
2746 * @author Leo Fajardo (@leorw)
2747 * @since 2.1.4
2748 *
2749 * @param number $plugin_id
2750 *
2751 * @return object
2752 */
2753 private function cancel_subscription_or_trial( $plugin_id ) {
2754 $fs = null;
2755 if ( $plugin_id == $this->get_id() ) {
2756 $fs = $this;
2757 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2758 $fs = self::get_instance_by_id( $plugin_id );
2759 }
2760
2761 $result = null;
2762
2763 if ( ! is_null( $fs ) ) {
2764 $result = $fs->is_paid_trial() ?
2765 $fs->_cancel_trial() :
2766 $fs->_downgrade_site();
2767 }
2768
2769 return $result;
2770 }
2771
2772 /**
2773 * @author Leo Fajardo (@leorw)
2774 * @since 2.0.2
2775 */
2776 function _delete_theme_update_data_action() {
2777 FS_Plugin_Updater::instance( $this )->delete_update_data();
2778 }
2779
2780 #endregion
2781
2782 #----------------------------------------------------------------------------------
2783 #region Instance
2784 #----------------------------------------------------------------------------------
2785
2786 /**
2787 * Main singleton instance.
2788 *
2789 * @author Vova Feldman (@svovaf)
2790 * @since 1.0.0
2791 *
2792 * @param number $module_id
2793 * @param string|bool $slug
2794 * @param bool $is_init Is initiation sequence.
2795 *
2796 * @return Freemius|false
2797 */
2798 static function instance( $module_id, $slug = false, $is_init = false ) {
2799 if ( empty( $module_id ) ) {
2800 return false;
2801 }
2802
2803 /**
2804 * 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.
2805 */
2806 self::_load_required_static();
2807
2808 if ( ! is_numeric( $module_id ) ) {
2809 if ( ! $is_init && true === $slug ) {
2810 $is_init = true;
2811 }
2812
2813 $slug = $module_id;
2814
2815 $module = FS_Plugin_Manager::instance( $slug )->get();
2816
2817 if ( is_object( $module ) ) {
2818 $module_id = $module->id;
2819 }
2820 }
2821
2822 $key = 'm_' . $module_id;
2823
2824 if ( ! isset( self::$_instances[ $key ] ) ) {
2825 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2826 }
2827
2828 return self::$_instances[ $key ];
2829 }
2830
2831 /**
2832 * @author Vova Feldman (@svovaf)
2833 * @since 1.0.6
2834 *
2835 * @param number $addon_id
2836 *
2837 * @return bool
2838 */
2839 private static function has_instance( $addon_id ) {
2840 return isset( self::$_instances[ 'm_' . $addon_id ] );
2841 }
2842
2843 /**
2844 * @author Leo Fajardo (@leorw)
2845 * @since 1.2.2
2846 *
2847 * @param string|number $id_or_slug
2848 * @param string $module_type
2849 *
2850 * @return number|false
2851 */
2852 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2853 if ( is_numeric( $id_or_slug ) ) {
2854 return $id_or_slug;
2855 }
2856
2857 foreach ( self::$_instances as $instance ) {
2858 // Also check the module type since there can be a plugin and a theme with the same slug.
2859 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2860 return $instance->get_id();
2861 }
2862 }
2863
2864 return false;
2865 }
2866
2867 /**
2868 * @author Vova Feldman (@svovaf)
2869 * @since 1.0.6
2870 *
2871 * @param number $id
2872 *
2873 * @return false|Freemius
2874 */
2875 static function get_instance_by_id( $id ) {
2876 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2877 self::$_instances[ 'm_' . $id ] :
2878 false;
2879 }
2880
2881 /**
2882 *
2883 * @author Vova Feldman (@svovaf)
2884 * @since 1.0.1
2885 *
2886 * @param string $plugin_file
2887 * @param string $module_type
2888 *
2889 * @return false|Freemius
2890 */
2891 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2892 $slug = self::find_slug_by_basename( $plugin_file );
2893
2894 return ( false !== $slug ) ?
2895 self::instance( self::get_module_id( $slug, $module_type ) ) :
2896 false;
2897 }
2898
2899 /**
2900 * @author Vova Feldman (@svovaf)
2901 * @since 1.0.6
2902 *
2903 * @return false|Freemius
2904 */
2905 function get_parent_instance() {
2906 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2907 }
2908
2909 /**
2910 * @author Vova Feldman (@svovaf)
2911 * @since 1.0.6
2912 *
2913 * @param string|number $id_or_slug
2914 *
2915 * @return false|Freemius
2916 */
2917 function get_addon_instance( $id_or_slug ) {
2918 $addon_id = self::get_module_id( $id_or_slug );
2919
2920 return self::instance( $addon_id );
2921 }
2922
2923 #endregion ------------------------------------------------------------------
2924
2925 /**
2926 * @author Vova Feldman (@svovaf)
2927 * @since 1.0.6
2928 *
2929 * @return bool
2930 */
2931 function is_parent_plugin_installed() {
2932 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2933
2934 if ( $is_active ) {
2935 return true;
2936 }
2937
2938 /**
2939 * Parent module might be a theme. If that's the case, the add-on's FS
2940 * instance will be loaded prior to the theme's FS instance, therefore,
2941 * we need to check if it's active with a "look ahead".
2942 *
2943 * @author Vova Feldman
2944 * @since 1.2.2.3
2945 */
2946 global $fs_active_plugins;
2947 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2948 $active_theme = wp_get_theme();
2949
2950 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2951 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2952 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2953 // Parent module is a theme and it's currently active.
2954 return true;
2955 }
2956 }
2957 }
2958 }
2959
2960 return false;
2961 }
2962
2963 /**
2964 * Check if add-on parent plugin in activation mode.
2965 *
2966 * @author Vova Feldman (@svovaf)
2967 * @since 1.0.7
2968 *
2969 * @return bool
2970 */
2971 function is_parent_in_activation() {
2972 $parent_fs = $this->get_parent_instance();
2973 if ( ! is_object( $parent_fs ) ) {
2974 return false;
2975 }
2976
2977 return ( $parent_fs->is_activation_mode() );
2978 }
2979
2980 /**
2981 * Is plugin in activation mode.
2982 *
2983 * @author Vova Feldman (@svovaf)
2984 * @since 1.0.7
2985 *
2986 * @param bool $and_on
2987 *
2988 * @return bool
2989 */
2990 function is_activation_mode( $and_on = true ) {
2991 return fs_is_network_admin() ?
2992 $this->is_network_activation_mode( $and_on ) :
2993 $this->is_site_activation_mode( $and_on );
2994 }
2995
2996 /**
2997 * Is plugin in activation mode.
2998 *
2999 * @author Vova Feldman (@svovaf)
3000 * @since 1.0.7
3001 *
3002 * @param bool $and_on
3003 *
3004 * @return bool
3005 */
3006 function is_site_activation_mode( $and_on = true ) {
3007 return (
3008 ( $this->is_on() || ! $and_on ) &&
3009 (
3010 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
3011 (
3012 ( ! $this->is_registered() ||
3013 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
3014 ( ! $this->is_enable_anonymous() ||
3015 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
3016 )
3017 )
3018 );
3019 }
3020
3021 /**
3022 * Checks if the SDK in network activation mode.
3023 *
3024 * @author Leo Fajardo (@leorw)
3025 * @since 2.0.0
3026 *
3027 * @param bool $and_on
3028 *
3029 * @return bool
3030 */
3031 private function is_network_activation_mode( $and_on = true ) {
3032 if ( ! $this->_is_network_active ) {
3033 // Not network activated.
3034 return false;
3035 }
3036
3037 if ( $this->is_network_upgrade_mode() ) {
3038 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3039 return true;
3040 }
3041
3042 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3043 // 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.
3044 return false;
3045 }
3046
3047 if ( $this->is_network_delegated_connection() ) {
3048 // Super-admin delegated the connection to the site admins -> not activation mode.
3049 return false;
3050 }
3051
3052 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3053 // Super-admin skipped the connection network wide -> not activation mode.
3054 return false;
3055 }
3056
3057 if ( $this->is_network_registered() ) {
3058 // Super-admin connected at least one site -> not activation mode.
3059 return false;
3060 }
3061
3062 return true;
3063 }
3064
3065 /**
3066 * Check if current page is the opt-in/pending-activation page.
3067 *
3068 * @author Vova Feldman (@svovaf)
3069 * @since 1.2.1.7
3070 *
3071 * @return bool
3072 */
3073 function is_activation_page() {
3074 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3075 return true;
3076 }
3077
3078 if ( ! $this->is_activation_mode() ) {
3079 return false;
3080 }
3081
3082 // Check if current page is matching the activation page.
3083 return $this->is_matching_url( $this->get_activation_url() );
3084 }
3085
3086 /**
3087 * Check if URL path's are matching and that all querystring
3088 * arguments of the $sub_url exist in the $url with the same values.
3089 *
3090 * WARNING:
3091 * 1. This method doesn't check if the sub/domain are matching.
3092 * 2. Ignore case sensitivity.
3093 *
3094 * @author Vova Feldman (@svovaf)
3095 * @since 1.2.1.7
3096 *
3097 * @param string $sub_url
3098 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3099 *
3100 * @return bool
3101 */
3102 private function is_matching_url( $sub_url, $url = '' ) {
3103 if ( empty( $url ) ) {
3104 $url = $_SERVER['REQUEST_URI'];
3105 }
3106
3107 $url = strtolower( $url );
3108 $sub_url = strtolower( $sub_url );
3109
3110 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3111 // Different path - DO NOT OVERRIDE PAGE.
3112 return false;
3113 }
3114
3115 $url_params = array();
3116 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
3117
3118 $sub_url_params = array();
3119 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
3120
3121 foreach ( $sub_url_params as $key => $val ) {
3122 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3123 // Not matching query string - DO NOT OVERRIDE PAGE.
3124 return false;
3125 }
3126 }
3127
3128 return true;
3129 }
3130
3131 /**
3132 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3133 *
3134 * @author Vova Feldman (@svovaf)
3135 * @since 2.0.0
3136 *
3137 * @param int $blog_id
3138 *
3139 * @return string[]
3140 */
3141 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3142 if ( is_multisite() && $blog_id > 0 ) {
3143 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3144 } else {
3145 $active_basenames = get_option( 'active_plugins' );
3146 }
3147
3148 if ( ! is_array( $active_basenames ) ) {
3149 $active_basenames = array();
3150 }
3151
3152 if ( is_multisite() ) {
3153 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3154
3155 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3156 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3157 }
3158 }
3159
3160 return $active_basenames;
3161 }
3162
3163 /**
3164 * @author Leo Fajardo (@leorw)
3165 * @since 2.3.0
3166 *
3167 * @param int $blog_id
3168 *
3169 * @return array
3170 */
3171 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3172 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3173
3174 $map = array();
3175
3176 foreach ( $active_basenames as $active_basename ) {
3177 $active_basename = fs_normalize_path( $active_basename );
3178
3179 if ( false === strpos( $active_basename, '/' ) ) {
3180 continue;
3181 }
3182
3183 $map[ dirname( $active_basename ) ] = true;
3184 }
3185
3186 return $map;
3187 }
3188
3189 /**
3190 * Get collection of all active plugins. Including network activated plugins.
3191 *
3192 * @author Vova Feldman (@svovaf)
3193 * @since 1.0.9
3194 *
3195 * @param int $blog_id Since 2.0.0
3196 *
3197 * @return array[string]array
3198 */
3199 private static function get_active_plugins( $blog_id = 0 ) {
3200 self::require_plugin_essentials();
3201
3202 $active_plugin = array();
3203 $all_plugins = fs_get_plugins();
3204 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3205
3206 foreach ( $active_plugins_basenames as $plugin_basename ) {
3207 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3208 }
3209
3210 return $active_plugin;
3211 }
3212
3213 /**
3214 * Get collection of all site active plugins for a specified blog.
3215 *
3216 * @author Vova Feldman (@svovaf)
3217 * @since 2.0.0
3218 *
3219 * @param int $blog_id
3220 *
3221 * @return array[string]array
3222 */
3223 private static function get_site_active_plugins( $blog_id = 0 ) {
3224 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3225 get_blog_option( $blog_id, 'active_plugins' ) :
3226 get_option( 'active_plugins' );
3227
3228 $active = array();
3229
3230 if ( ! is_array( $active_basenames ) ) {
3231 return $active;
3232 }
3233
3234 foreach ( $active_basenames as $basename ) {
3235 $active[ $basename ] = array(
3236 'is_active' => true,
3237 'Version' => '1.0', // Dummy version.
3238 'slug' => self::get_plugin_slug( $basename ),
3239 );
3240 }
3241
3242 return $active;
3243 }
3244
3245 /**
3246 * Get collection of all plugins with their activation status for a specified blog.
3247 *
3248 * @author Vova Feldman (@svovaf)
3249 * @since 1.1.8
3250 *
3251 * @param int $blog_id Since 2.0.0
3252 *
3253 * @return array Key is the plugin file path and the value is an array of the plugin data.
3254 */
3255 private static function get_all_plugins( $blog_id = 0 ) {
3256 self::require_plugin_essentials();
3257
3258 $all_plugins = fs_get_plugins();
3259
3260 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3261
3262 foreach ( $all_plugins as $basename => &$data ) {
3263 // By default set to inactive (next foreach update the active plugins).
3264 $data['is_active'] = false;
3265 // Enrich with plugin slug.
3266 $data['slug'] = self::get_plugin_slug( $basename );
3267 }
3268
3269 // Flag active plugins.
3270 foreach ( $active_plugins_basenames as $basename ) {
3271 if ( isset( $all_plugins[ $basename ] ) ) {
3272 $all_plugins[ $basename ]['is_active'] = true;
3273 }
3274 }
3275
3276 return $all_plugins;
3277 }
3278
3279 /**
3280 * Get collection of all plugins and if they are network level activated.
3281 *
3282 * @author Vova Feldman (@svovaf)
3283 * @since 2.0.0
3284 *
3285 * @return array Key is the plugin basename and the value is an array of the plugin data.
3286 */
3287 private static function get_network_plugins() {
3288 self::require_plugin_essentials();
3289
3290 $all_plugins = fs_get_plugins();
3291
3292 $network_active_basenames = is_multisite() ?
3293 get_site_option( 'active_sitewide_plugins' ) :
3294 array();
3295
3296 foreach ( $all_plugins as $basename => &$data ) {
3297 // By default set to inactive (next foreach update the active plugins).
3298 $data['is_active'] = false;
3299 // Enrich with plugin slug.
3300 $data['slug'] = self::get_plugin_slug( $basename );
3301 }
3302
3303 // Flag active plugins.
3304 foreach ( $network_active_basenames as $basename ) {
3305 if ( isset( $all_plugins[ $basename ] ) ) {
3306 $all_plugins[ $basename ]['is_active'] = true;
3307 }
3308 }
3309
3310 return $all_plugins;
3311 }
3312
3313 /**
3314 * Cached result of get_site_transient( 'update_plugins' )
3315 *
3316 * @author Vova Feldman (@svovaf)
3317 * @since 1.1.8
3318 *
3319 * @var object
3320 */
3321 private static $_plugins_info;
3322
3323 /**
3324 * Helper function to get specified plugin's slug.
3325 *
3326 * @author Vova Feldman (@svovaf)
3327 * @since 1.1.8
3328 *
3329 * @param $basename
3330 *
3331 * @return string
3332 */
3333 private static function get_plugin_slug( $basename ) {
3334 if ( ! isset( self::$_plugins_info ) ) {
3335 self::$_plugins_info = get_site_transient( 'update_plugins' );
3336 }
3337
3338 $slug = '';
3339
3340 if ( is_object( self::$_plugins_info ) ) {
3341 if ( isset( self::$_plugins_info->no_update ) &&
3342 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3343 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3344 ) {
3345 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3346 } else if ( isset( self::$_plugins_info->response ) &&
3347 isset( self::$_plugins_info->response[ $basename ] ) &&
3348 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3349 ) {
3350 $slug = self::$_plugins_info->response[ $basename ]->slug;
3351 }
3352 }
3353
3354 if ( empty( $slug ) ) {
3355 // Try to find slug from FS data.
3356 $slug = self::find_slug_by_basename( $basename );
3357 }
3358
3359 if ( empty( $slug ) ) {
3360 // Fallback to plugin's folder name.
3361 $slug = dirname( $basename );
3362 }
3363
3364 return $slug;
3365 }
3366
3367 private static $_statics_loaded = false;
3368
3369 /**
3370 * Load static resources.
3371 *
3372 * @author Vova Feldman (@svovaf)
3373 * @since 1.0.1
3374 */
3375 private static function _load_required_static() {
3376 if ( self::$_statics_loaded ) {
3377 return;
3378 }
3379
3380 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3381
3382 self::$_static_logger->entrance();
3383
3384 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3385
3386 if ( is_multisite() ) {
3387 $has_skipped_migration = (
3388 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3389 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3390 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3391 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3392 );
3393
3394 /**
3395 * If the file_slug_map exists on the site level but doesn't exist on the
3396 * network level storage, it means that we need to process the storage with migration.
3397 *
3398 * 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.
3399 *
3400 * @author Vova Feldman (@svovaf)
3401 * @since 2.0.0
3402 */
3403 if (
3404 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3405 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3406 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3407 ) {
3408 self::migrate_options_to_network();
3409 }
3410 }
3411
3412 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3413
3414 if ( ! WP_FS__DEMO_MODE ) {
3415 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3416 'Freemius',
3417 '_add_debug_section'
3418 ) );
3419 }
3420
3421 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3422
3423 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3424
3425 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3426
3427 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3428
3429 if ( 0 == did_action( 'plugins_loaded' ) ) {
3430 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3431 }
3432
3433 add_action( 'admin_footer', array( 'Freemius', '_enrich_ajax_url' ) );
3434 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3435
3436 if ( self::is_plugins_page() || self::is_themes_page() ) {
3437 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3438
3439 /**
3440 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3441 * page.
3442 *
3443 * @author Leo Fajardo (@leorw)
3444 * @since 2.3.0
3445 */
3446 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3447 }
3448
3449 self::$_statics_loaded = true;
3450 }
3451
3452 /**
3453 * @author Leo Fajardo (@leorw)
3454 *
3455 * @since 2.1.3
3456 */
3457 private static function migrate_options_to_network() {
3458 self::migrate_accounts_to_network();
3459
3460 // Migrate API options from site level to network level.
3461 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3462 $api_network_options->migrate_to_network();
3463
3464 // Migrate API cache to network level storage.
3465 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3466
3467 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3468 }
3469
3470 #----------------------------------------------------------------------------------
3471 #region Localization
3472 #----------------------------------------------------------------------------------
3473
3474 /**
3475 * Load framework's text domain.
3476 *
3477 * @author Vova Feldman (@svovaf)
3478 * @since 1.2.1
3479 */
3480 static function _load_textdomain() {
3481 if ( ! is_admin() ) {
3482 return;
3483 }
3484
3485 global $fs_active_plugins;
3486
3487 // Works both for plugins and themes.
3488 load_plugin_textdomain(
3489 'freemius',
3490 false,
3491 $fs_active_plugins->newest->sdk_path . '/languages/'
3492 );
3493 }
3494
3495 #endregion
3496
3497 #----------------------------------------------------------------------------------
3498 #region Debugging
3499 #----------------------------------------------------------------------------------
3500
3501 /**
3502 * @author Vova Feldman (@svovaf)
3503 * @since 1.0.8
3504 */
3505 static function _add_debug_section() {
3506 if ( ! is_super_admin() ) {
3507 // Add debug page only for super-admins.
3508 return;
3509 }
3510
3511 self::$_static_logger->entrance();
3512
3513 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3514
3515 if ( WP_FS__DEV_MODE ) {
3516 // Add top-level debug menu item.
3517 $hook = FS_Admin_Menu_Manager::add_page(
3518 $title,
3519 $title,
3520 'manage_options',
3521 'freemius',
3522 array( 'Freemius', '_debug_page_render' )
3523 );
3524 } else {
3525 // Add hidden debug page.
3526 $hook = FS_Admin_Menu_Manager::add_subpage(
3527 null,
3528 $title,
3529 $title,
3530 'manage_options',
3531 'freemius',
3532 array( 'Freemius', '_debug_page_render' )
3533 );
3534 }
3535
3536 if ( ! empty( $hook ) ) {
3537 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3538 }
3539 }
3540
3541 /**
3542 * @author Vova Feldman (@svovaf)
3543 * @since 1.1.7.3
3544 */
3545 static function _toggle_debug_mode() {
3546 if ( ! is_super_admin() ) {
3547 return;
3548 }
3549
3550 $is_on = fs_request_get( 'is_on', false, 'post' );
3551
3552 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3553 update_option( 'fs_debug_mode', $is_on );
3554
3555 // Turn on/off storage logging.
3556 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3557 }
3558
3559 exit;
3560 }
3561
3562 /**
3563 * @author Vova Feldman (@svovaf)
3564 * @since 1.2.1.6
3565 */
3566 static function _get_debug_log() {
3567 $logs = FS_Logger::load_db_logs(
3568 fs_request_get( 'filters', false, 'post' ),
3569 ! empty( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? $_POST['limit'] : 200,
3570 ! empty( $_POST['offset'] ) && is_numeric( $_POST['offset'] ) ? $_POST['offset'] : 0
3571 );
3572
3573 self::shoot_ajax_success( $logs );
3574 }
3575
3576 /**
3577 * @author Vova Feldman (@svovaf)
3578 * @since 1.2.1.7
3579 */
3580 static function _get_db_option() {
3581 check_admin_referer( 'fs_get_db_option' );
3582
3583 $option_name = fs_request_get( 'option_name' );
3584
3585 if ( ! is_super_admin() ||
3586 ! fs_starts_with( $option_name, 'fs_' )
3587 ) {
3588 self::shoot_ajax_failure();
3589 }
3590
3591 $value = get_option( $option_name );
3592
3593 $result = array(
3594 'name' => $option_name,
3595 );
3596
3597 if ( false !== $value ) {
3598 if ( ! is_string( $value ) ) {
3599 $value = json_encode( $value );
3600 }
3601
3602 $result['value'] = $value;
3603 }
3604
3605 self::shoot_ajax_success( $result );
3606 }
3607
3608 /**
3609 * @author Vova Feldman (@svovaf)
3610 * @since 1.2.1.7
3611 */
3612 static function _set_db_option() {
3613 check_admin_referer( 'fs_set_db_option' );
3614
3615 $option_name = fs_request_get( 'option_name' );
3616
3617 if ( ! is_super_admin() ||
3618 ! fs_starts_with( $option_name, 'fs_' )
3619 ) {
3620 self::shoot_ajax_failure();
3621 }
3622
3623 $option_value = fs_request_get( 'option_value' );
3624
3625 if ( ! empty( $option_value ) ) {
3626 update_option( $option_name, $option_value );
3627 }
3628
3629 self::shoot_ajax_success();
3630 }
3631
3632 /**
3633 * @author Vova Feldman (@svovaf)
3634 * @since 1.0.8
3635 */
3636 static function _debug_page_actions() {
3637 self::_clean_admin_content_section();
3638
3639 if ( fs_request_is_action( 'restart_freemius' ) ) {
3640 check_admin_referer( 'restart_freemius' );
3641
3642 if ( ! is_multisite() ) {
3643 // Clear accounts data.
3644 self::$_accounts->clear( null, true );
3645 } else {
3646 $sites = self::get_sites();
3647 foreach ( $sites as $site ) {
3648 $blog_id = self::get_site_blog_id( $site );
3649 self::$_accounts->clear( $blog_id, true );
3650 }
3651
3652 // Clear network level storage.
3653 self::$_accounts->clear( true, true );
3654 }
3655
3656 // Clear SDK reference cache.
3657 delete_option( 'fs_active_plugins' );
3658 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3659 check_admin_referer( 'clear_updates_data' );
3660
3661 if ( ! is_multisite() ) {
3662 set_site_transient( 'update_plugins', null );
3663 set_site_transient( 'update_themes', null );
3664 } else {
3665 $current_blog_id = get_current_blog_id();
3666
3667 $sites = self::get_sites();
3668 foreach ( $sites as $site ) {
3669 switch_to_blog( self::get_site_blog_id( $site ) );
3670
3671 set_site_transient( 'update_plugins', null );
3672 set_site_transient( 'update_themes', null );
3673 }
3674
3675 switch_to_blog( $current_blog_id );
3676 }
3677 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3678 check_admin_referer( 'simulate_trial' );
3679
3680 $fs = freemius( fs_request_get( 'module_id' ) );
3681
3682 // Update SDK install to at least 24 hours before.
3683 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3684 // Unset the trial shown timestamp.
3685 unset( $fs->_storage->trial_promotion_shown );
3686 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3687 check_admin_referer( 'simulate_network_upgrade' );
3688
3689 $fs = freemius( fs_request_get( 'module_id' ) );
3690
3691 self::set_network_upgrade_mode( $fs->_storage );
3692 } else if ( fs_request_is_action( 'delete_install' ) ) {
3693 check_admin_referer( 'delete_install' );
3694
3695 self::_delete_site_by_slug(
3696 fs_request_get( 'slug' ),
3697 fs_request_get( 'module_type' ),
3698 true,
3699 fs_request_get( 'blog_id', null )
3700 );
3701 } else if ( fs_request_is_action( 'delete_user' ) ) {
3702 check_admin_referer( 'delete_user' );
3703
3704 self::delete_user( fs_request_get( 'user_id' ) );
3705 } else if ( fs_request_is_action( 'download_logs' ) ) {
3706 check_admin_referer( 'download_logs' );
3707
3708 $download_url = FS_Logger::download_db_logs(
3709 fs_request_get( 'filters', false, 'post' )
3710 );
3711
3712 if ( false === $download_url ) {
3713 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.' );
3714 }
3715
3716 fs_redirect( $download_url );
3717 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3718 check_admin_referer( 'migrate_options_to_network' );
3719
3720 self::migrate_options_to_network();
3721 }
3722 }
3723
3724 /**
3725 * @author Vova Feldman (@svovaf)
3726 * @since 1.0.8
3727 */
3728 static function _debug_page_render() {
3729 self::$_static_logger->entrance();
3730
3731 if ( ! is_multisite() ) {
3732 $all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
3733 $all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME );
3734 } else {
3735 $sites = self::get_sites();
3736
3737 $all_plugins_installs = array();
3738 $all_themes_installs = array();
3739
3740 foreach ( $sites as $site ) {
3741 $blog_id = self::get_site_blog_id( $site );
3742
3743 $plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
3744
3745 foreach ( $plugins_installs as $slug => $install ) {
3746 if ( ! isset( $all_plugins_installs[ $slug ] ) ) {
3747 $all_plugins_installs[ $slug ] = array();
3748 }
3749
3750 $install->blog_id = $blog_id;
3751
3752 $all_plugins_installs[ $slug ][] = $install;
3753 }
3754
3755 $themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id );
3756
3757 foreach ( $themes_installs as $slug => $install ) {
3758 if ( ! isset( $all_themes_installs[ $slug ] ) ) {
3759 $all_themes_installs[ $slug ] = array();
3760 }
3761
3762 $install->blog_id = $blog_id;
3763
3764 $all_themes_installs[ $slug ][] = $install;
3765 }
3766 }
3767 }
3768
3769 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3770
3771 $vars = array(
3772 'plugin_sites' => $all_plugins_installs,
3773 'theme_sites' => $all_themes_installs,
3774 'users' => self::get_all_users(),
3775 'addons' => self::get_all_addons(),
3776 'account_addons' => self::get_all_account_addons(),
3777 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3778 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3779 );
3780
3781 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3782 fs_require_once_template( 'debug.php', $vars );
3783 }
3784
3785 #endregion
3786
3787 #----------------------------------------------------------------------------------
3788 #region Connectivity Issues
3789 #----------------------------------------------------------------------------------
3790
3791 /**
3792 * Check if Freemius should be turned on for the current plugin install.
3793 *
3794 * Note:
3795 * $this->_is_on is updated in has_api_connectivity()
3796 *
3797 * @author Vova Feldman (@svovaf)
3798 * @since 1.0.9
3799 *
3800 * @return bool
3801 */
3802 function is_on() {
3803 self::$_static_logger->entrance();
3804
3805 if ( isset( $this->_is_on ) ) {
3806 return $this->_is_on;
3807 }
3808
3809 // If already installed or pending then sure it's on :)
3810 if ( $this->is_registered() || $this->is_pending_activation() ) {
3811 $this->_is_on = true;
3812
3813 return true;
3814 }
3815
3816 return false;
3817 }
3818
3819 /**
3820 * @author Vova Feldman (@svovaf)
3821 * @since 1.1.7.3
3822 *
3823 * @param bool $flush_if_no_connectivity
3824 *
3825 * @return bool
3826 */
3827 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3828 if ( ! isset( $this->_storage->connectivity_test ) ) {
3829 // Connectivity test was never executed, or cache was cleared.
3830 return true;
3831 }
3832
3833 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3834 if ( WP_FS__IS_HTTP_REQUEST ) {
3835 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3836 // Domain changed.
3837 return true;
3838 }
3839
3840 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3841 // Server IP changed.
3842 return true;
3843 }
3844 }
3845 }
3846
3847 if ( $this->_storage->connectivity_test['is_connected'] &&
3848 $this->_storage->connectivity_test['is_active']
3849 ) {
3850 // API connected and Freemius is active - no need to run connectivity check.
3851 return false;
3852 }
3853
3854 if ( $flush_if_no_connectivity ) {
3855 /**
3856 * If explicitly asked to flush when no connectivity - do it only
3857 * if at least 10 sec passed from the last API connectivity test.
3858 */
3859 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3860 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3861 }
3862
3863 /**
3864 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3865 */
3866 $version = $this->get_plugin_version();
3867 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3868 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3869 return true;
3870 }
3871
3872 return false;
3873 }
3874
3875 /**
3876 * @author Vova Feldman (@svovaf)
3877 * @since 1.1.7.4
3878 *
3879 * @param int|null $blog_id Since 2.0.0.
3880 * @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test.
3881 *
3882 * @return object|false
3883 */
3884 private function ping( $blog_id = null, $is_gdpr_test = false ) {
3885 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
3886 return false;
3887 }
3888
3889 $version = $this->get_plugin_version();
3890
3891 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
3892
3893 return $this->get_api_plugin_scope()->ping(
3894 $this->get_anonymous_id( $blog_id ),
3895 array(
3896 'is_update' => json_encode( $is_update ),
3897 'version' => $version,
3898 'sdk' => $this->version,
3899 'is_admin' => json_encode( is_admin() ),
3900 'is_ajax' => json_encode( self::is_ajax() ),
3901 'is_cron' => json_encode( self::is_cron() ),
3902 'is_gdpr_test' => $is_gdpr_test,
3903 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
3904 )
3905 );
3906 }
3907
3908 /**
3909 * Check if there's any connectivity issue to Freemius API.
3910 *
3911 * @author Vova Feldman (@svovaf)
3912 * @since 1.0.9
3913 *
3914 * @param bool $flush_if_no_connectivity
3915 *
3916 * @return bool
3917 */
3918 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3919 $this->_logger->entrance();
3920
3921 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3922 return $this->_has_api_connection;
3923 }
3924
3925 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3926 isset( $this->_storage->connectivity_test ) &&
3927 true === $this->_storage->connectivity_test['is_connected']
3928 ) {
3929 unset( $this->_storage->connectivity_test );
3930 }
3931
3932 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3933 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3934 /**
3935 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3936 *
3937 * @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.
3938 */
3939 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3940 $this->is_premium() ||
3941 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3942
3943 return $this->_has_api_connection;
3944 }
3945
3946 $pong = $this->ping();
3947 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3948
3949 if ( ! $is_connected ) {
3950 // API failure.
3951 $this->_add_connectivity_issue_message( $pong );
3952 }
3953
3954 if ( $is_connected ) {
3955 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
3956 }
3957
3958 $this->store_connectivity_info( $pong, $is_connected );
3959
3960 return $this->_has_api_connection;
3961 }
3962
3963 /**
3964 * @author Vova Feldman (@svovaf)
3965 * @since 1.1.7.4
3966 *
3967 * @param object $pong
3968 * @param bool $is_connected
3969 */
3970 private function store_connectivity_info( $pong, $is_connected ) {
3971 $this->_logger->entrance();
3972
3973 $version = $this->get_plugin_version();
3974
3975 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3976 $is_active = false;
3977 } else {
3978 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3979 }
3980
3981 $is_active = $this->apply_filters(
3982 'is_on',
3983 $is_active,
3984 $this->is_plugin_update(),
3985 $version
3986 );
3987
3988 $this->_storage->connectivity_test = array(
3989 'is_connected' => $is_connected,
3990 'host' => $_SERVER['HTTP_HOST'],
3991 'server_ip' => WP_FS__REMOTE_ADDR,
3992 'is_active' => $is_active,
3993 'timestamp' => WP_FS__SCRIPT_START_TIME,
3994 // Last version with connectivity attempt.
3995 'version' => $version,
3996 );
3997
3998 $this->_has_api_connection = $is_connected;
3999 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4000 }
4001
4002 /**
4003 * Force turning Freemius on.
4004 *
4005 * @author Vova Feldman (@svovaf)
4006 * @since 1.1.8.1
4007 *
4008 * @return bool TRUE if successfully turned on.
4009 */
4010 private function turn_on() {
4011 $this->_logger->entrance();
4012
4013 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4014 return false;
4015 }
4016
4017 $updated_connectivity = $this->_storage->connectivity_test;
4018 $updated_connectivity['is_active'] = true;
4019 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4020 $this->_storage->connectivity_test = $updated_connectivity;
4021
4022 $this->_is_on = true;
4023
4024 return true;
4025 }
4026
4027 /**
4028 * Anonymous and unique site identifier (Hash).
4029 *
4030 * @author Vova Feldman (@svovaf)
4031 * @since 1.1.0
4032 *
4033 * @param null|int $blog_id Since 2.0.0
4034 *
4035 * @return string
4036 */
4037 function get_anonymous_id( $blog_id = null ) {
4038 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4039
4040 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4041 $key = fs_strip_url_protocol( get_site_url( $blog_id ) );
4042
4043 $secure_auth = SECURE_AUTH_KEY;
4044 if ( empty( $secure_auth ) ||
4045 false !== strpos( $secure_auth, ' ' ) ||
4046 'put your unique phrase here' === $secure_auth
4047 ) {
4048 // Protect against default auth key.
4049 $secure_auth = md5( microtime() );
4050 }
4051
4052 /**
4053 * Base the unique identifier on the WP secure authentication key. Which
4054 * turns the key into a secret anonymous identifier. This will help us
4055 * to avoid duplicate installs generation on the backend upon opt-in.
4056 *
4057 * @author Vova Feldman (@svovaf)
4058 * @since 1.2.3
4059 */
4060 $unique_id = md5( $key . $secure_auth );
4061
4062 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4063 }
4064
4065 $this->_logger->departure( $unique_id );
4066
4067 return $unique_id;
4068 }
4069
4070 /**
4071 * @author Vova Feldman (@svovaf)
4072 * @since 1.1.7.4
4073 *
4074 * @return \WP_User
4075 */
4076 static function _get_current_wp_user() {
4077 self::require_pluggable_essentials();
4078 self::wp_cookie_constants();
4079
4080 return wp_get_current_user();
4081 }
4082
4083 /**
4084 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4085 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4086 * is network activated the cookie constants are only configured after the network
4087 * plugins activation, therefore, if we don't define those constants WP will throw
4088 * PHP warnings/notices.
4089 *
4090 * @author Vova Feldman (@svovaf)
4091 * @since 2.1.1
4092 */
4093 private static function wp_cookie_constants() {
4094 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4095 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4096 ) {
4097 return;
4098 }
4099
4100 /**
4101 * Used to guarantee unique hash cookies
4102 *
4103 * @since 1.5.0
4104 */
4105 if ( ! defined( 'COOKIEHASH' ) ) {
4106 $siteurl = get_site_option( 'siteurl' );
4107 if ( $siteurl ) {
4108 define( 'COOKIEHASH', md5( $siteurl ) );
4109 } else {
4110 define( 'COOKIEHASH', '' );
4111 }
4112 }
4113
4114 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4115 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4116 }
4117
4118 /**
4119 * @since 2.5.0
4120 */
4121 if ( ! defined( 'AUTH_COOKIE' ) ) {
4122 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4123 }
4124
4125 /**
4126 * @since 2.6.0
4127 */
4128 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4129 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4130 }
4131 }
4132
4133 /**
4134 * @author Vova Feldman (@svovaf)
4135 * @since 2.1.0
4136 *
4137 * @return int
4138 */
4139 static function get_current_wp_user_id() {
4140 $wp_user = self::_get_current_wp_user();
4141
4142 return $wp_user->ID;
4143 }
4144
4145 /**
4146 * @author Vova Feldman (@svovaf)
4147 * @since 1.2.1.7
4148 *
4149 * @param string $email
4150 *
4151 * @return bool
4152 */
4153 static function is_valid_email( $email ) {
4154 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4155 return false;
4156 }
4157
4158 $parts = explode( '@', $email );
4159
4160 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4161 return false;
4162 }
4163
4164 $blacklist = array(
4165 'admin.',
4166 'webmaster.',
4167 'localhost.',
4168 'dev.',
4169 'development.',
4170 'test.',
4171 'stage.',
4172 'staging.',
4173 );
4174
4175 // Make sure domain is not one of the blacklisted.
4176 foreach ( $blacklist as $invalid ) {
4177 if ( 0 === strpos( $parts[1], $invalid ) ) {
4178 return false;
4179 }
4180 }
4181
4182 // Get the UTF encoded domain name.
4183 $domain = idn_to_ascii( $parts[1] ) . '.';
4184
4185 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4186 }
4187
4188 /**
4189 * Generate API connectivity issue message.
4190 *
4191 * @author Vova Feldman (@svovaf)
4192 * @since 1.0.9
4193 *
4194 * @param mixed $api_result
4195 * @param bool $is_first_failure
4196 */
4197 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
4198 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
4199 // Don't add message if it's the free version and can run anonymously.
4200 return;
4201 }
4202
4203 if ( ! function_exists( 'wp_nonce_url' ) ) {
4204 require_once ABSPATH . 'wp-includes/functions.php';
4205 }
4206
4207 $current_user = self::_get_current_wp_user();
4208 // $admin_email = get_option( 'admin_email' );
4209 $admin_email = $current_user->user_email;
4210
4211 // Aliases.
4212 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
4213 $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' );
4214 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
4215 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
4216 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
4217 $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' );
4218 /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
4219 $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
4220 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
4221 $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' );
4222
4223 $message = false;
4224 if ( is_object( $api_result ) &&
4225 isset( $api_result->error ) &&
4226 isset( $api_result->error->code )
4227 ) {
4228 switch ( $api_result->error->code ) {
4229 case 'curl_missing':
4230 $missing_methods = '';
4231 if ( is_array( $api_result->missing_methods ) &&
4232 ! empty( $api_result->missing_methods )
4233 ) {
4234 foreach ( $api_result->missing_methods as $m ) {
4235 if ( 'curl_version' === $m ) {
4236 continue;
4237 }
4238
4239 if ( ! empty( $missing_methods ) ) {
4240 $missing_methods .= ', ';
4241 }
4242
4243 $missing_methods .= sprintf( '<code>%s</code>', $m );
4244 }
4245
4246 if ( ! empty( $missing_methods ) ) {
4247 $missing_methods = sprintf(
4248 '<br><br><b>%s</b> %s',
4249 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
4250 $missing_methods
4251 );
4252 }
4253 }
4254
4255 $message = sprintf(
4256 $x_requires_access_to_api . ' ' .
4257 $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' ) . ' ' .
4258 $missing_methods .
4259 ' %s',
4260 '<b>' . $this->get_plugin_name() . '</b>',
4261 sprintf(
4262 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4263 sprintf(
4264 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
4265 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
4266 ' - ' . sprintf(
4267 $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' ),
4268 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4269 )
4270 ),
4271 sprintf(
4272 '<b>%s</b> - %s',
4273 $sysadmin_title,
4274 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 ) ) )
4275 ),
4276 sprintf(
4277 '<a href="%s"><b>%s</b></a> - %s',
4278 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 ),
4279 $deactivate_plugin_title,
4280 $deactivate_plugin_desc
4281 )
4282 )
4283 );
4284 break;
4285 case 'cloudflare_ddos_protection':
4286 $message = sprintf(
4287 $x_requires_access_to_api . ' ' .
4288 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
4289 $happy_to_resolve_issue_asap .
4290 ' %s',
4291 '<b>' . $this->get_plugin_name() . '</b>',
4292 sprintf(
4293 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4294 sprintf(
4295 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
4296 $fix_issue_title,
4297 ' - ' . sprintf(
4298 $fix_issue_desc,
4299 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4300 )
4301 ),
4302 sprintf(
4303 '<a href="%s" target="_blank" rel="noopener noreferrer"><b>%s</b></a> - %s',
4304 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4305 $install_previous_title,
4306 $install_previous_desc
4307 ),
4308 sprintf(
4309 '<a href="%s"><b>%s</b></a> - %s',
4310 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 ),
4311 $deactivate_plugin_title,
4312 $deactivate_plugin_desc
4313 )
4314 )
4315 );
4316 break;
4317 case 'squid_cache_block':
4318 $message = sprintf(
4319 $x_requires_access_to_api . ' ' .
4320 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
4321 ' %s',
4322 '<b>' . $this->get_plugin_name() . '</b>',
4323 sprintf(
4324 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4325 sprintf(
4326 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
4327 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
4328 sprintf(
4329 $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' ),
4330 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4331 )
4332 ),
4333 sprintf(
4334 '<b>%s</b> - %s',
4335 $sysadmin_title,
4336 sprintf(
4337 $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' ),
4338 // We use a filter since the plugin might require additional API connectivity.
4339 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
4340 'api.freemius.com',
4341 'wp.freemius.com'
4342 ) ) ) . '</b>',
4343 $this->_module_type
4344 )
4345 ),
4346 sprintf(
4347 '<a href="%s"><b>%s</b></a> - %s',
4348 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 ),
4349 $deactivate_plugin_title,
4350 $deactivate_plugin_desc
4351 )
4352 )
4353 );
4354 break;
4355 // default:
4356 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
4357 // break;
4358 }
4359 }
4360
4361 $message_id = 'failed_connect_api';
4362 $type = 'error';
4363
4364 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
4365
4366 if ( false === $message ) {
4367 if ( $is_first_failure ) {
4368 // First attempt failed.
4369 $message = sprintf(
4370 $x_requires_access_to_api . ' ' .
4371 $connectivity_test_fails_message . ' ' .
4372 $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>' .
4373 '%s',
4374 '<b>' . $this->get_plugin_name() . '</b>',
4375 sprintf(
4376 '<div id="fs_firewall_issue_options">%s %s</div>',
4377 sprintf(
4378 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
4379 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
4380 ),
4381 sprintf(
4382 '<a href="%s" class="button">%s</a>',
4383 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 ),
4384 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
4385 )
4386 )
4387 );
4388
4389 $message_id = 'failed_connect_api_first';
4390 $type = 'promotion';
4391 } else {
4392 // Second connectivity attempt failed.
4393 $message = sprintf(
4394 $x_requires_access_to_api . ' ' .
4395 $connectivity_test_fails_message . ' ' .
4396 $happy_to_resolve_issue_asap .
4397 ' %s',
4398 '<b>' . $this->get_plugin_name() . '</b>',
4399 sprintf(
4400 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4401 sprintf(
4402 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
4403 $fix_issue_title,
4404 ' - ' . sprintf(
4405 $fix_issue_desc,
4406 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4407 )
4408 ),
4409 sprintf(
4410 '<a href="%s" target="_blank" rel="noopener noreferrer"><b>%s</b></a> - %s',
4411 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4412 $install_previous_title,
4413 $install_previous_desc
4414 ),
4415 sprintf(
4416 '<a href="%s"><b>%s</b></a> - %s',
4417 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 ),
4418 $deactivate_plugin_title,
4419 $deactivate_plugin_desc
4420 )
4421 )
4422 );
4423 }
4424 }
4425
4426 $this->_admin_notices->add_sticky(
4427 $message,
4428 $message_id,
4429 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4430 $type
4431 );
4432 }
4433
4434 /**
4435 * Handle user request to resolve connectivity issue.
4436 * This method will send an email to Freemius API technical staff for resolution.
4437 * The email will contain server's info and installed plugins (might be caching issue).
4438 *
4439 * @author Vova Feldman (@svovaf)
4440 * @since 1.0.9
4441 */
4442 function _email_about_firewall_issue() {
4443 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
4444
4445 $pong = $this->ping();
4446
4447 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4448
4449 if ( $is_connected ) {
4450 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4451
4452 $this->store_connectivity_info( $pong, $is_connected );
4453
4454 echo $this->get_after_plugin_activation_redirect_url();
4455 exit;
4456 }
4457
4458 $current_user = self::_get_current_wp_user();
4459 $admin_email = $current_user->user_email;
4460
4461 $error_type = fs_request_get( 'error_type', 'general' );
4462
4463 switch ( $error_type ) {
4464 case 'squid':
4465 $title = 'Squid ACL Blocking Issue';
4466 break;
4467 case 'cloudflare':
4468 $title = 'CloudFlare Blocking Issue';
4469 break;
4470 default:
4471 $title = 'API Connectivity Issue';
4472 break;
4473 }
4474
4475 $custom_email_sections = array();
4476
4477 // Add 'API Error' custom email section.
4478 $custom_email_sections['api_error'] = array(
4479 'title' => 'API Error',
4480 'rows' => array(
4481 'ping' => array(
4482 'API Error',
4483 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
4484 ),
4485 )
4486 );
4487
4488 // Send email with technical details to resolve API connectivity issues.
4489 $this->send_email(
4490 'api@freemius.com', // recipient
4491 $title . ' [' . $this->get_plugin_name() . ']', // subject
4492 $custom_email_sections,
4493 array( "Reply-To: $admin_email <$admin_email>" ) // headers
4494 );
4495
4496 $this->_admin_notices->add_sticky(
4497 sprintf(
4498 $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' ),
4499 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4500 ),
4501 'server_details_sent'
4502 );
4503
4504 // Action was taken, tell that API connectivity troubleshooting should be off now.
4505
4506 echo "1";
4507 exit;
4508 }
4509
4510 /**
4511 * Handle connectivity test retry approved by the user.
4512 *
4513 * @author Vova Feldman (@svovaf)
4514 * @since 1.1.7.4
4515 */
4516 function _retry_connectivity_test() {
4517 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
4518
4519 $pong = $this->ping();
4520
4521 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4522
4523 if ( $is_connected ) {
4524 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4525
4526 $this->store_connectivity_info( $pong, $is_connected );
4527
4528 echo $this->get_after_plugin_activation_redirect_url();
4529 } else {
4530 // Add connectivity issue message after 2nd failed attempt.
4531 $this->_add_connectivity_issue_message( $pong, false );
4532
4533 echo "1";
4534 }
4535
4536 exit;
4537 }
4538
4539 static function _add_firewall_issues_javascript() {
4540 $params = array();
4541 fs_require_once_template( 'firewall-issues-js.php', $params );
4542 }
4543
4544 #endregion
4545
4546 #----------------------------------------------------------------------------------
4547 #region Email
4548 #----------------------------------------------------------------------------------
4549
4550 /**
4551 * Generates and sends an HTML email with customizable sections.
4552 *
4553 * @author Leo Fajardo (@leorw)
4554 * @since 1.1.2
4555 *
4556 * @param string $to_address
4557 * @param string $subject
4558 * @param array $sections
4559 * @param array $headers
4560 *
4561 * @return bool Whether the email contents were sent successfully.
4562 */
4563 private function send_email(
4564 $to_address,
4565 $subject,
4566 $sections = array(),
4567 $headers = array()
4568 ) {
4569 $default_sections = $this->get_email_sections();
4570
4571 // Insert new sections or replace the default email sections.
4572 if ( is_array( $sections ) && ! empty( $sections ) ) {
4573 foreach ( $sections as $section_id => $custom_section ) {
4574 if ( ! isset( $default_sections[ $section_id ] ) ) {
4575 // If the section does not exist, add it.
4576 $default_sections[ $section_id ] = $custom_section;
4577 } else {
4578 // If the section already exists, override it.
4579 $current_section = $default_sections[ $section_id ];
4580
4581 // Replace the current section's title if a custom section title exists.
4582 if ( isset( $custom_section['title'] ) ) {
4583 $current_section['title'] = $custom_section['title'];
4584 }
4585
4586 // Insert new rows under the current section or replace the default rows.
4587 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4588 foreach ( $custom_section['rows'] as $row_id => $row ) {
4589 $current_section['rows'][ $row_id ] = $row;
4590 }
4591 }
4592
4593 $default_sections[ $section_id ] = $current_section;
4594 }
4595 }
4596 }
4597
4598 $vars = array( 'sections' => $default_sections );
4599 $message = fs_get_template( 'email.php', $vars );
4600
4601 // Set the type of email to HTML.
4602 $headers[] = 'Content-type: text/html; charset=UTF-8';
4603
4604 $header_string = implode( "\r\n", $headers );
4605
4606 return wp_mail(
4607 $to_address,
4608 $subject,
4609 $message,
4610 $header_string
4611 );
4612 }
4613
4614 /**
4615 * Generates the data for the sections of the email content.
4616 *
4617 * @author Leo Fajardo (@leorw)
4618 * @since 1.1.2
4619 *
4620 * @return array
4621 */
4622 private function get_email_sections() {
4623 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4624 $current_user = self::_get_current_wp_user();
4625
4626 // Retrieve the cURL version information so that we can get the version number below.
4627 $curl_version_information = curl_version();
4628
4629 $active_plugin = self::get_active_plugins();
4630
4631 // Generate the list of active plugins separated by new line.
4632 $active_plugin_string = '';
4633 foreach ( $active_plugin as $plugin ) {
4634 $active_plugin_string .= sprintf(
4635 '<a href="%s">%s</a> [v%s]<br>',
4636 $plugin['PluginURI'],
4637 $plugin['Name'],
4638 $plugin['Version']
4639 );
4640 }
4641
4642 $server_ip = WP_FS__REMOTE_ADDR;
4643
4644 // Add PHP info for deeper investigation.
4645 ob_start();
4646 phpinfo();
4647 $php_info = ob_get_clean();
4648
4649 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4650
4651 // Generate the default email sections.
4652 $sections = array(
4653 'sdk' => array(
4654 'title' => 'SDK',
4655 'rows' => array(
4656 'fs_version' => array( 'FS Version', $this->version ),
4657 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4658 )
4659 ),
4660 'plugin' => array(
4661 'title' => ucfirst( $this->get_module_type() ),
4662 'rows' => array(
4663 'name' => array( 'Name', $this->get_plugin_name() ),
4664 'version' => array( 'Version', $this->get_plugin_version() )
4665 )
4666 ),
4667 'api' => array(
4668 'title' => 'API Subdomain',
4669 'rows' => array(
4670 'dns' => array(
4671 'DNS_CNAME',
4672 function_exists( 'dns_get_record' ) ?
4673 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4674 'dns_get_record() disabled/blocked'
4675 ),
4676 'ip' => array(
4677 'IP',
4678 function_exists( 'gethostbyname' ) ?
4679 gethostbyname( $api_domain ) :
4680 'gethostbyname() disabled/blocked'
4681 ),
4682 ),
4683 ),
4684 'site' => array(
4685 'title' => 'Site',
4686 'rows' => array(
4687 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4688 'address' => array( 'Address', site_url() ),
4689 'host' => array(
4690 'HTTP_HOST',
4691 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4692 ),
4693 'hosting' => array(
4694 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4695 fs_request_get( 'hosting_company' ) :
4696 'Unknown',
4697 ),
4698 'server_addr' => array(
4699 'SERVER_ADDR',
4700 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4701 )
4702 )
4703 ),
4704 'user' => array(
4705 'title' => 'User',
4706 'rows' => array(
4707 'email' => array( 'Email', $current_user->user_email ),
4708 'first' => array( 'First', $current_user->user_firstname ),
4709 'last' => array( 'Last', $current_user->user_lastname )
4710 )
4711 ),
4712 'plugins' => array(
4713 'title' => 'Plugins',
4714 'rows' => array(
4715 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4716 )
4717 ),
4718 'php_info' => array(
4719 'title' => 'PHP Info',
4720 'rows' => array(
4721 'info' => array( $php_info )
4722 ),
4723 )
4724 );
4725
4726 // Allow the sections to be modified by other code.
4727 $sections = $this->apply_filters( 'email_template_sections', $sections );
4728
4729 return $sections;
4730 }
4731
4732 #endregion
4733
4734 #----------------------------------------------------------------------------------
4735 #region Initialization
4736 #----------------------------------------------------------------------------------
4737
4738 /**
4739 * Init plugin's Freemius instance.
4740 *
4741 * @author Vova Feldman (@svovaf)
4742 * @since 1.0.1
4743 *
4744 * @param number $id
4745 * @param string $public_key
4746 * @param bool $is_live
4747 * @param bool $is_premium
4748 */
4749 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4750 $this->_logger->entrance();
4751
4752 $this->dynamic_init( array(
4753 'id' => $id,
4754 'public_key' => $public_key,
4755 'is_live' => $is_live,
4756 'is_premium' => $is_premium,
4757 ) );
4758 }
4759
4760 /**
4761 * Dynamic initiator, originally created to support initiation
4762 * with parent_id for add-ons.
4763 *
4764 * @author Vova Feldman (@svovaf)
4765 * @since 1.0.6
4766 *
4767 * @param array $plugin_info
4768 *
4769 * @throws Freemius_Exception
4770 */
4771 function dynamic_init( array $plugin_info ) {
4772 $this->_logger->entrance();
4773
4774 $this->parse_settings( $plugin_info );
4775
4776 $this->register_after_settings_parse_hooks();
4777
4778 if ( $this->should_stop_execution() ) {
4779 return;
4780 }
4781
4782 if ( ! $this->is_registered() ) {
4783 if ( $this->is_anonymous() ) {
4784 // If user skipped, no need to test connectivity.
4785 $this->_has_api_connection = true;
4786 $this->_is_on = true;
4787 } else {
4788 if ( ! $this->has_api_connectivity() ) {
4789 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
4790 $this->_admin_notices->has_sticky( 'failed_connect_api' )
4791 ) {
4792 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
4793 // If anonymous mode is disabled, add firewall admin-notice message.
4794 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
4795
4796 $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' );
4797 add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array(
4798 &$this,
4799 '_email_about_firewall_issue'
4800 ) );
4801
4802 add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array(
4803 &$this,
4804 '_retry_connectivity_test'
4805 ) );
4806
4807 /**
4808 * 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.
4809 *
4810 * @author Vova Feldman (@svovaf)
4811 * @since 2.0.0
4812 */
4813 /*$this->add_ajax_action( 'resolve_firewall_issues', array(
4814 &$this,
4815 '_email_about_firewall_issue'
4816 ) );
4817
4818 $this->add_ajax_action( 'retry_connectivity_test', array(
4819 &$this,
4820 '_retry_connectivity_test'
4821 ) );*/
4822 }
4823 }
4824
4825 return;
4826 } else {
4827 $this->_admin_notices->remove_sticky( array(
4828 'failed_connect_api_first',
4829 'failed_connect_api',
4830 ) );
4831
4832 if ( $this->_anonymous_mode ) {
4833 // Simulate anonymous mode.
4834 $this->_is_anonymous = true;
4835 }
4836 }
4837 }
4838 }
4839
4840 /**
4841 * This should be executed even if Freemius is off for the core module,
4842 * otherwise, the add-ons dialogbox won't work properly. This is esepcially
4843 * relevant when the developer decided to turn FS off for existing users.
4844 *
4845 * @author Vova Feldman (@svovaf)
4846 */
4847 if ( $this->is_user_in_admin() &&
4848 'plugin-information' === fs_request_get( 'tab', false ) &&
4849 $this->should_use_freemius_updater_and_dialog() &&
4850 (
4851 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4852 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4853 )
4854 ) {
4855 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4856
4857 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4858 }
4859
4860 // Check if Freemius is on for the current plugin.
4861 // This MUST be executed after all the plugin variables has been loaded.
4862 if ( ! $this->is_registered() && ! $this->is_on() ) {
4863 return;
4864 }
4865
4866 if ( $this->has_api_connectivity() ) {
4867 if ( self::is_cron() ) {
4868 $this->hook_callback_to_sync_cron();
4869 } else if ( $this->is_user_in_admin() ) {
4870 /**
4871 * Schedule daily data sync cron if:
4872 *
4873 * 1. User opted-in (for tracking).
4874 * 2. If skipped, but later upgraded (opted-in via upgrade).
4875 *
4876 * @author Vova Feldman (@svovaf)
4877 * @since 1.1.7.3
4878 *
4879 */
4880 if ( $this->is_registered() ) {
4881 if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) {
4882 $this->schedule_sync_cron();
4883 }
4884 }
4885
4886 /**
4887 * Check if requested for manual blocking background sync.
4888 */
4889 if ( fs_request_has( 'background_sync' ) ) {
4890 $this->run_manual_sync();
4891 }
4892 }
4893 }
4894
4895 if ( $this->is_registered() ) {
4896 $this->hook_callback_to_install_sync();
4897 }
4898
4899 if ( $this->is_addon() ) {
4900 if ( $this->is_parent_plugin_installed() ) {
4901 // Link to parent FS.
4902 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4903
4904 // Get parent plugin reference.
4905 $this->_parent_plugin = $this->_parent->get_plugin();
4906 }
4907 }
4908
4909 if ( $this->is_user_in_admin() ) {
4910 if ( $this->is_addon() ) {
4911 if ( ! $this->is_parent_plugin_installed() ) {
4912 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4913
4914 if ( isset( $plugin_info['parent'] ) ) {
4915 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4916 }
4917
4918 $this->_admin_notices->add(
4919 ( ! empty( $parent_name ) ?
4920 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 ) :
4921 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() )
4922 ),
4923 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4924 'error'
4925 );
4926
4927 return;
4928 } else {
4929 $is_network_admin = fs_is_network_admin();
4930
4931 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4932 // If add-on activated and parent not, automatically install parent for the user.
4933 $this->activate_parent_account( $this->_parent );
4934 } else if (
4935 $this->_parent->is_registered() &&
4936 ! $this->is_registered() &&
4937 /**
4938 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4939 * * Network active and in network admin - network activate add-on account.
4940 * * Network active and not in network admin - activate add-on account for the current blog.
4941 * * Not network active and not in network admin - activate add-on account for the current blog.
4942 *
4943 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4944 *
4945 * @author Leo Fajardo (@leorw)
4946 * @since 2.3.0
4947 */
4948 ( $this->is_network_active() || ! $is_network_admin )
4949 ) {
4950 $premium_license = null;
4951
4952 if (
4953 ! $this->has_free_plan() &&
4954 $this->is_bundle_license_auto_activation_enabled() &&
4955 $this->_parent->is_activated_with_bundle_license()
4956 ) {
4957 /**
4958 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4959 *
4960 * @author Leo Fajardo (@leorw)
4961 * @since 2.4.0
4962 */
4963 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4964
4965 if (
4966 is_object( $bundle_license ) &&
4967 ! empty( $bundle_license->products ) &&
4968 in_array( $this->get_id(), $bundle_license->products )
4969 ) {
4970 $premium_license = $bundle_license;
4971 }
4972 }
4973
4974 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4975 // If parent plugin activated, automatically install add-on for the user.
4976 $this->_activate_addon_account(
4977 $this->_parent,
4978 ( $this->is_network_active() && $is_network_admin ) ?
4979 true :
4980 get_current_blog_id(),
4981 $premium_license
4982 );
4983 }
4984 }
4985
4986 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4987 if ( $this->is_premium() ) {
4988 // Remove add-on download admin-notice.
4989 $this->_parent->_admin_notices->remove_sticky( array(
4990 'addon_plan_upgraded_' . $this->_slug,
4991 'no_addon_license_' . $this->_slug,
4992 ) );
4993 }
4994
4995 // $this->deactivate_premium_only_addon_without_license();
4996 }
4997 }
4998
4999 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
5000
5001 // if ( $this->is_registered() ||
5002 // $this->is_anonymous() ||
5003 // $this->is_pending_activation()
5004 // ) {
5005 // $this->_init_admin();
5006 // }
5007 }
5008
5009 /**
5010 * Should be called outside `$this->is_user_in_admin()` scope
5011 * because the updater has some logic that needs to be executed
5012 * during AJAX calls.
5013 *
5014 * Currently we need to hook to the `http_request_host_is_external` filter.
5015 * In the future, there might be additional logic added.
5016 *
5017 * @author Vova Feldman
5018 * @since 1.2.1.6
5019 */
5020 if (
5021 $this->should_use_freemius_updater_and_dialog() &&
5022 (
5023 $this->is_premium() ||
5024 /**
5025 * If not premium but the premium version is installed, also instantiate the updater so that the
5026 * plugin information dialog of the premium version will have the information from the server.
5027 *
5028 * @author Leo Fajardo (@leorw)
5029 * @since 2.2.3
5030 */
5031 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
5032 ) &&
5033 $this->has_release_on_freemius()
5034 ) {
5035 FS_Plugin_Updater::instance( $this );
5036 }
5037
5038 $this->do_action( 'initiated' );
5039
5040 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
5041 if ( isset( $this->_storage->prev_is_premium ) ) {
5042 $this->apply_filters(
5043 'after_code_type_change',
5044 // New code type.
5045 $this->_plugin->is_premium
5046 );
5047 } else {
5048 // Set for code type for the first time.
5049 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5050 }
5051 }
5052
5053 if ( ! $this->is_addon() ) {
5054 if ( $this->is_registered() ) {
5055 // Fix for upgrade from versions < 1.0.9.
5056 if ( ! isset( $this->_storage->activation_timestamp ) ) {
5057 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
5058 }
5059
5060 $this->do_action( 'after_init_plugin_registered' );
5061 } else if ( $this->is_anonymous() ) {
5062 $this->do_action( 'after_init_plugin_anonymous' );
5063 } else if ( $this->is_pending_activation() ) {
5064 $this->do_action( 'after_init_plugin_pending_activations' );
5065 }
5066 } else {
5067 if ( $this->is_registered() ) {
5068 $this->do_action( 'after_init_addon_registered' );
5069 } else if ( $this->is_anonymous() ) {
5070 $this->do_action( 'after_init_addon_anonymous' );
5071 } else if ( $this->is_pending_activation() ) {
5072 $this->do_action( 'after_init_addon_pending_activations' );
5073 }
5074 }
5075 }
5076
5077 /**
5078 * @author Leo Fajardo (@leorw)
5079 * @since 2.2.3
5080 *
5081 * @return bool
5082 */
5083 private function should_use_freemius_updater_and_dialog() {
5084 return (
5085 /**
5086 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5087 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5088 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5089 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5090 * plugin details from .org).
5091 */
5092 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5093 (
5094 ! self::is_plugin_install_page() &&
5095 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5096 ( 'install-plugin' !== fs_request_get( 'action' ) )
5097 )
5098 );
5099 }
5100
5101 /**
5102 * @author Leo Fajardo (@leorw)
5103 *
5104 * @since 1.2.1.5
5105 */
5106 function _stop_tracking_callback() {
5107 $this->_logger->entrance();
5108
5109 $this->check_ajax_referer( 'stop_tracking' );
5110
5111 $result = $this->stop_tracking( fs_is_network_admin() );
5112
5113 if ( true === $result ) {
5114 self::shoot_ajax_success();
5115 }
5116
5117 $this->_logger->api_error( $result );
5118
5119 self::shoot_ajax_failure(
5120 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
5121 ( $this->is_api_error( $result ) && isset( $result->error ) ?
5122 $result->error->message :
5123 var_export( $result, true ) )
5124 );
5125 }
5126
5127 /**
5128 * @author Leo Fajardo (@leorw)
5129 * @since 1.2.1.5
5130 */
5131 function _allow_tracking_callback() {
5132 $this->_logger->entrance();
5133
5134 $this->check_ajax_referer( 'allow_tracking' );
5135
5136 $result = $this->allow_tracking( fs_is_network_admin() );
5137
5138 if ( true === $result ) {
5139 self::shoot_ajax_success();
5140 }
5141
5142 $this->_logger->api_error( $result );
5143
5144 self::shoot_ajax_failure(
5145 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
5146 ( $this->is_api_error( $result ) && isset( $result->error ) ?
5147 $result->error->message :
5148 var_export( $result, true ) )
5149 );
5150 }
5151
5152 /**
5153 * Opt-out from usage tracking.
5154 *
5155 * Note: This will not delete the account information but will stop all tracking.
5156 *
5157 * Returns:
5158 * 1. FALSE - If the user never opted-in.
5159 * 2. TRUE - If successfully opted-out.
5160 * 3. object - API result on failure.
5161 *
5162 * @author Leo Fajardo (@leorw)
5163 * @since 1.2.1.5
5164 *
5165 * @return bool|object
5166 */
5167 function stop_site_tracking() {
5168 $this->_logger->entrance();
5169
5170 if ( ! $this->is_registered() ) {
5171 // User never opted-in.
5172 return false;
5173 }
5174
5175 if ( $this->is_tracking_prohibited() ) {
5176 // Already disconnected.
5177 return true;
5178 }
5179
5180 // Send update to FS.
5181 $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array(
5182 'is_disconnected' => true
5183 ) );
5184
5185 if ( ! $this->is_api_result_entity( $result ) ||
5186 ! isset( $result->is_disconnected ) ||
5187 ! $result->is_disconnected
5188 ) {
5189 $this->_logger->api_error( $result );
5190
5191 return $result;
5192 }
5193
5194 $this->_site->is_disconnected = $result->is_disconnected;
5195 $this->_store_site();
5196
5197 $this->clear_sync_cron();
5198
5199 // Successfully disconnected.
5200 return true;
5201 }
5202
5203 /**
5204 * Opt-out network from usage tracking.
5205 *
5206 * Note: This will not delete the account information but will stop all tracking.
5207 *
5208 * Returns:
5209 * 1. FALSE - If the user never opted-in.
5210 * 2. TRUE - If successfully opted-out.
5211 * 3. object - API result on failure.
5212 *
5213 * @author Leo Fajardo (@leorw)
5214 * @since 1.2.1.5
5215 *
5216 * @return bool|object
5217 */
5218 function stop_network_tracking() {
5219 $this->_logger->entrance();
5220
5221 if ( ! $this->is_registered() ) {
5222 // User never opted-in.
5223 return false;
5224 }
5225
5226 $install_id_2_blog_id = array();
5227 $installs_map = $this->get_blog_install_map();
5228
5229 $opt_out_all = true;
5230
5231 $params = array();
5232 foreach ( $installs_map as $blog_id => $install ) {
5233 if ( $install->is_tracking_prohibited() ) {
5234 // Already opted-out.
5235 continue;
5236 }
5237
5238 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5239 // Opt-out only from non-delegated installs.
5240 $opt_out_all = false;
5241 continue;
5242 }
5243
5244 $params[] = array( 'id' => $install->id );
5245
5246 $install_id_2_blog_id[ $install->id ] = $blog_id;
5247 }
5248
5249 if ( empty( $install_id_2_blog_id ) ) {
5250 return true;
5251 }
5252
5253 $params[] = array( 'is_disconnected' => true );
5254
5255 // Send update to FS.
5256 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5257
5258 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5259 $this->_logger->api_error( $result );
5260
5261 return $result;
5262 }
5263
5264 foreach ( $result->installs as $r_install ) {
5265 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5266 $install = $installs_map[ $blog_id ];
5267 $install->is_disconnected = $r_install->is_disconnected;
5268 $this->_store_site( true, $blog_id, $install );
5269 }
5270
5271 $this->clear_sync_cron( $opt_out_all );
5272
5273 // Successfully disconnected.
5274 return true;
5275 }
5276
5277 /**
5278 * Opt-out from usage tracking.
5279 *
5280 * Note: This will not delete the account information but will stop all tracking.
5281 *
5282 * Returns:
5283 * 1. FALSE - If the user never opted-in.
5284 * 2. TRUE - If successfully opted-out.
5285 * 3. object - API result on failure.
5286 *
5287 * @author Leo Fajardo (@leorw)
5288 * @since 1.2.1.5
5289 *
5290 * @param bool $is_network_action
5291 *
5292 * @return bool|object
5293 */
5294 function stop_tracking( $is_network_action = false ) {
5295 $this->_logger->entrance();
5296
5297 return $is_network_action ?
5298 $this->stop_network_tracking() :
5299 $this->stop_site_tracking();
5300 }
5301
5302 /**
5303 * Opt-in back into usage tracking.
5304 *
5305 * Note: This will only work if the user opted-in previously.
5306 *
5307 * Returns:
5308 * 1. FALSE - If the user never opted-in.
5309 * 2. TRUE - If successfully opted-in back to usage tracking.
5310 * 3. object - API result on failure.
5311 *
5312 * @author Leo Fajardo (@leorw)
5313 * @since 1.2.1.5
5314 *
5315 * @return bool|object
5316 */
5317 function allow_site_tracking() {
5318 $this->_logger->entrance();
5319
5320 if ( ! $this->is_registered() ) {
5321 // User never opted-in.
5322 return false;
5323 }
5324
5325 if ( $this->is_tracking_allowed() ) {
5326 // Tracking already allowed.
5327 return true;
5328 }
5329
5330 $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array(
5331 'is_disconnected' => false
5332 ) );
5333
5334 if ( ! $this->is_api_result_entity( $result ) ||
5335 ! isset( $result->is_disconnected ) ||
5336 $result->is_disconnected
5337 ) {
5338 $this->_logger->api_error( $result );
5339
5340 return $result;
5341 }
5342
5343 $this->_site->is_disconnected = $result->is_disconnected;
5344 $this->_store_site();
5345
5346 $this->schedule_sync_cron();
5347
5348 // Successfully reconnected.
5349 return true;
5350 }
5351
5352 /**
5353 * Opt-in network back into usage tracking.
5354 *
5355 * Note: This will only work if the user opted-in previously.
5356 *
5357 * Returns:
5358 * 1. FALSE - If the user never opted-in.
5359 * 2. TRUE - If successfully opted-in back to usage tracking.
5360 * 3. object - API result on failure.
5361 *
5362 * @author Leo Fajardo (@leorw)
5363 * @since 1.2.1.5
5364 *
5365 * @return bool|object
5366 */
5367 function allow_network_tracking() {
5368 $this->_logger->entrance();
5369
5370 if ( ! $this->is_registered() ) {
5371 // User never opted-in.
5372 return false;
5373 }
5374
5375 $install_id_2_blog_id = array();
5376 $installs_map = $this->get_blog_install_map();
5377
5378 $params = array();
5379 foreach ( $installs_map as $blog_id => $install ) {
5380 if ( $install->is_tracking_allowed() ) {
5381 continue;
5382 }
5383
5384 $params[] = array( 'id' => $install->id );
5385
5386 $install_id_2_blog_id[ $install->id ] = $blog_id;
5387 }
5388
5389 if ( empty( $install_id_2_blog_id ) ) {
5390 return true;
5391 }
5392
5393 $params[] = array( 'is_disconnected' => false );
5394
5395 // Send update to FS.
5396 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5397
5398
5399 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5400 $this->_logger->api_error( $result );
5401
5402 return $result;
5403 }
5404
5405 foreach ( $result->installs as $r_install ) {
5406 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5407 $install = $installs_map[ $blog_id ];
5408 $install->is_disconnected = $r_install->is_disconnected;
5409 $this->_store_site( true, $blog_id, $install );
5410 }
5411
5412 $this->schedule_sync_cron();
5413
5414 // Successfully reconnected.
5415 return true;
5416 }
5417
5418 /**
5419 * Opt-in back into usage tracking.
5420 *
5421 * Note: This will only work if the user opted-in previously.
5422 *
5423 * Returns:
5424 * 1. FALSE - If the user never opted-in.
5425 * 2. TRUE - If successfully opted-in back to usage tracking.
5426 * 3. object - API result on failure.
5427 *
5428 * @author Leo Fajardo (@leorw)
5429 * @since 1.2.1.5
5430 *
5431 * @param bool $is_network_action
5432 *
5433 * @return bool|object
5434 */
5435 function allow_tracking( $is_network_action = false ) {
5436 $this->_logger->entrance();
5437
5438 return $is_network_action ?
5439 $this->allow_network_tracking() :
5440 $this->allow_site_tracking();
5441 }
5442
5443 /**
5444 * If user opted-in and later disabled usage-tracking,
5445 * re-allow tracking for licensing and updates.
5446 *
5447 * @author Leo Fajardo (@leorw)
5448 * @since 1.2.1.5
5449 *
5450 * @param bool $is_context_single_site
5451 */
5452 private function reconnect_locally( $is_context_single_site = false ) {
5453 $this->_logger->entrance();
5454
5455 if ( ! $this->is_registered() ) {
5456 return;
5457 }
5458
5459 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5460 if ( $this->is_tracking_prohibited() ) {
5461 $this->_site->is_disconnected = false;
5462 $this->_store_site();
5463 }
5464 } else {
5465 $installs_map = $this->get_blog_install_map();
5466 foreach ( $installs_map as $blog_id => $install ) {
5467 /**
5468 * @var FS_Site $install
5469 */
5470 if ( $install->is_tracking_prohibited() ) {
5471 $install->is_disconnected = false;
5472 $this->_store_site( true, $blog_id, $install );
5473 }
5474 }
5475 }
5476 }
5477
5478 /**
5479 * @author Vova Feldman (@svovaf)
5480 * @since 2.3.2
5481 *
5482 * @return bool
5483 */
5484 function is_extensions_tracking_allowed() {
5485 return ( true === $this->apply_filters(
5486 'is_extensions_tracking_allowed',
5487 $this->_storage->get( 'is_extensions_tracking_allowed', true )
5488 ) );
5489 }
5490
5491 /**
5492 * @author Vova Feldman (@svovaf)
5493 * @since 2.3.2
5494 */
5495 function _update_tracking_permission_callback() {
5496 $this->_logger->entrance();
5497
5498 $this->check_ajax_referer( 'update_tracking_permission' );
5499
5500 $is_enabled = fs_request_get_bool( 'is_enabled', null );
5501
5502 if ( ! is_bool( $is_enabled ) ) {
5503 self::shoot_ajax_failure();
5504 }
5505
5506 $permission = fs_request_get( 'permission' );
5507
5508 switch ( $permission ) {
5509 case 'extensions':
5510 $this->update_extensions_tracking_flag( $is_enabled );
5511 break;
5512 default:
5513 $permission = 'no_match';
5514 }
5515
5516 if ( 'no_match' === $permission ) {
5517 self::shoot_ajax_failure();
5518 }
5519
5520 self::shoot_ajax_success( array(
5521 'permissions' => array(
5522 $permission => $is_enabled,
5523 )
5524 ) );
5525 }
5526
5527 /**
5528 * @author Leo Fajardo (@leorw)
5529 * @since 2.3.2
5530 *
5531 * @param bool $is_enabled
5532 */
5533 private function update_extensions_tracking_flag( $is_enabled ) {
5534 $this->_storage->store( 'is_extensions_tracking_allowed', $is_enabled );
5535 }
5536
5537 /**
5538 * Parse plugin's settings (as defined by the plugin dev).
5539 *
5540 * @author Vova Feldman (@svovaf)
5541 * @since 1.1.7.3
5542 *
5543 * @param array $plugin_info
5544 *
5545 * @throws \Freemius_Exception
5546 */
5547 private function parse_settings( &$plugin_info ) {
5548 $this->_logger->entrance();
5549
5550 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5551 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5552 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5553 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5554 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5555
5556 /**
5557 * @author Vova Feldman (@svovaf)
5558 * @since 1.1.9 Try to pull secret key from external config.
5559 */
5560 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5561 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5562 }
5563
5564 if ( isset( $plugin_info['parent'] ) ) {
5565 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5566 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5567 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5568 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5569 }
5570
5571 if ( false === $id ) {
5572 throw new Freemius_Exception( array(
5573 'error' => array(
5574 'type' => 'ParameterNotSet',
5575 'message' => 'Plugin id parameter is not set.',
5576 'code' => 'plugin_id_not_set',
5577 'http' => 500,
5578 )
5579 ) );
5580 }
5581 if ( false === $public_key ) {
5582 throw new Freemius_Exception( array(
5583 'error' => array(
5584 'type' => 'ParameterNotSet',
5585 'message' => 'Plugin public_key parameter is not set.',
5586 'code' => 'plugin_public_key_not_set',
5587 'http' => 500,
5588 )
5589 ) );
5590 }
5591
5592 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5593 $this->_plugin :
5594 new FS_Plugin();
5595
5596 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5597
5598 $plugin->update( array(
5599 'id' => $id,
5600 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5601 'public_key' => $public_key,
5602 'slug' => $this->_slug,
5603 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5604 'parent_plugin_id' => $parent_id,
5605 'version' => $this->get_plugin_version(),
5606 'title' => $this->get_plugin_name( $premium_suffix ),
5607 'file' => $this->_plugin_basename,
5608 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5609 'premium_suffix' => $premium_suffix,
5610 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5611 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5612 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5613 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5614 ) );
5615
5616 if ( $plugin->is_updated() ) {
5617 // Update plugin details.
5618 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5619 }
5620 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5621 $this->_plugin->secret_key = $secret_key;
5622
5623 /**
5624 * 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).
5625 *
5626 * @author Vova Feldman
5627 * @since 2.4.5
5628 */
5629 if ( $this->is_network_active() && fs_is_network_admin() ) {
5630 if ( isset( $plugin_info['menu_network'] ) &&
5631 is_array( $plugin_info['menu_network'] ) &&
5632 ! empty( $plugin_info['menu_network'] )
5633 ) {
5634 $plugin_info['menu'] = $plugin_info['menu_network'];
5635 }
5636 }
5637
5638 if ( ! isset( $plugin_info['menu'] ) ) {
5639 $plugin_info['menu'] = array();
5640
5641 if ( ! empty( $this->_storage->sdk_last_version ) &&
5642 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5643 ) {
5644 // Backward compatibility to 1.1.2
5645 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5646 $plugin_info['menu_slug'] :
5647 $this->_slug;
5648 }
5649 }
5650
5651 $this->_menu = FS_Admin_Menu_Manager::instance(
5652 $this->_module_id,
5653 $this->_module_type,
5654 $this->get_unique_affix()
5655 );
5656
5657 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5658
5659 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5660 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5661 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5662 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5663 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5664 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5665 if ( $this->_is_premium_only ) {
5666 // If premium only plugin, disable anonymous mode.
5667 $this->_enable_anonymous = false;
5668 $this->_anonymous_mode = false;
5669 } else {
5670 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5671 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5672 }
5673 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5674 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5675
5676 if ( ! empty( $plugin_info['trial'] ) ) {
5677 $this->_trial_days = $this->get_numeric_option(
5678 $plugin_info['trial'],
5679 'days',
5680 // Default to 0 - trial without days specification.
5681 0
5682 );
5683
5684 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5685 }
5686
5687 $this->_navigation = $this->get_option(
5688 $plugin_info,
5689 'navigation',
5690 $this->is_free_wp_org_theme() ?
5691 self::NAVIGATION_TABS :
5692 self::NAVIGATION_MENU
5693 );
5694 }
5695
5696 /**
5697 * @param string[] $options
5698 * @param string $key
5699 * @param mixed $default
5700 *
5701 * @return bool
5702 */
5703 private function get_option( &$options, $key, $default = false ) {
5704 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5705 }
5706
5707 private function get_bool_option( &$options, $key, $default = false ) {
5708 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5709 }
5710
5711 private function get_numeric_option( &$options, $key, $default = false ) {
5712 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5713 }
5714
5715 /**
5716 * Gate keeper.
5717 *
5718 * @author Vova Feldman (@svovaf)
5719 * @since 1.1.7.3
5720 *
5721 * @return bool
5722 */
5723 private function should_stop_execution() {
5724 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5725 /**
5726 * Don't execute Freemius until plugin was fully loaded at least once,
5727 * to give the opportunity for the activation hook to run before pinging
5728 * the API for connectivity test. This logic is relevant for the
5729 * identification of new plugin install vs. plugin update.
5730 *
5731 * @author Vova Feldman (@svovaf)
5732 * @since 1.1.9
5733 */
5734 return true;
5735 }
5736
5737 if ( $this->is_activation_mode() ) {
5738 if ( ! is_admin() ) {
5739 /**
5740 * If in activation mode, don't execute Freemius outside of the
5741 * admin dashboard.
5742 *
5743 * @author Vova Feldman (@svovaf)
5744 * @since 1.1.7.3
5745 */
5746 return true;
5747 }
5748
5749 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5750 /**
5751 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5752 * then don't start Freemius.
5753 *
5754 * @author Vova Feldman (@svovaf)
5755 * @since 1.1.6.3
5756 *
5757 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5758 */
5759 return true;
5760 }
5761
5762 if ( self::is_cron() ) {
5763 /**
5764 * If in activation mode, don't execute Freemius during wp crons
5765 * (wp crons have HTTP context - called as HTTP request).
5766 *
5767 * @author Vova Feldman (@svovaf)
5768 * @since 1.1.7.3
5769 */
5770 return true;
5771 }
5772
5773 if ( self::is_ajax() &&
5774 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
5775 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
5776 ) {
5777 /**
5778 * During activation, if running in AJAX mode, unless there's a sticky
5779 * connectivity issue notice, don't run Freemius.
5780 *
5781 * @author Vova Feldman (@svovaf)
5782 * @since 1.1.7.3
5783 */
5784 return true;
5785 }
5786 }
5787
5788 return false;
5789 }
5790
5791 /**
5792 * Triggered after code type has changed.
5793 *
5794 * @author Vova Feldman (@svovaf)
5795 * @since 1.1.9.1
5796 */
5797 function _after_code_type_change() {
5798 $this->_logger->entrance();
5799
5800 if ( $this->is_theme() ) {
5801 // Expire the cache of the previous tabs since the theme may
5802 // have setting updates after code type has changed.
5803 $this->_cache->expire( 'tabs' );
5804 $this->_cache->expire( 'tabs_stylesheets' );
5805 }
5806
5807 if ( $this->is_registered() ) {
5808 if ( ! $this->is_addon() ) {
5809 add_action(
5810 is_admin() ? 'admin_init' : 'init',
5811 array( &$this, '_plugin_code_type_changed' )
5812 );
5813 }
5814
5815 if ( $this->is_premium() ) {
5816 // Purge cached payments after switching to the premium version.
5817 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5818 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5819 }
5820 }
5821 }
5822
5823 /**
5824 * Handles plugin's code type change (free <--> premium).
5825 *
5826 * @author Vova Feldman (@svovaf)
5827 * @since 1.0.9
5828 */
5829 function _plugin_code_type_changed() {
5830 $this->_logger->entrance();
5831
5832 if ( $this->is_premium() ) {
5833 $this->reconnect_locally();
5834
5835 // Activated premium code.
5836 $this->do_action( 'after_premium_version_activation' );
5837
5838 // Remove all sticky messages related to download of the premium version.
5839 $this->_admin_notices->remove_sticky( array(
5840 'trial_started',
5841 'plan_upgraded',
5842 'plan_changed',
5843 'license_activated',
5844 ) );
5845
5846 $notice = '';
5847 if ( ! $this->is_only_premium() ) {
5848 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5849 }
5850
5851 $license_notice = $this->get_license_network_activation_notice();
5852 if ( ! empty( $license_notice ) ) {
5853 $notice .= ' ' . $license_notice;
5854 }
5855
5856 if ( ! empty( $notice ) ) {
5857 $this->_admin_notices->add_sticky(
5858 trim( $notice ),
5859 'premium_activated',
5860 $this->get_text_x_inline( 'W00t',
5861 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5862 );
5863 }
5864 } else {
5865 // Remove sticky message related to premium code activation.
5866 $this->_admin_notices->remove_sticky( 'premium_activated' );
5867
5868 // Activated free code (after had the premium before).
5869 $this->do_action( 'after_free_version_reactivation' );
5870
5871 if ( $this->is_paying() && ! $this->is_premium() ) {
5872 $this->_admin_notices->add_sticky(
5873 sprintf(
5874 /* translators: %s: License type (e.g. you have a professional license) */
5875 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5876 $this->get_plan_title()
5877 ) . $this->get_complete_upgrade_instructions(),
5878 'plan_upgraded',
5879 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
5880 );
5881 }
5882 }
5883
5884 // Schedule code type changes event.
5885 $this->schedule_install_sync();
5886
5887 /**
5888 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5889 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5890 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5891 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5892 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5893 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5894 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5895 */
5896 $this->unregister_uninstall_hook();
5897
5898 $this->clear_module_main_file_cache();
5899
5900 // Update is_premium of latest version.
5901 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5902 }
5903
5904 #endregion
5905
5906 #----------------------------------------------------------------------------------
5907 #region Add-ons
5908 #----------------------------------------------------------------------------------
5909
5910 /**
5911 * Check if add-on installed and activated on site.
5912 *
5913 * @author Vova Feldman (@svovaf)
5914 * @since 1.0.6
5915 *
5916 * @param string|number $id_or_slug
5917 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5918 *
5919 * @return bool
5920 */
5921 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5922 $this->_logger->entrance();
5923
5924 $addon_id = self::get_module_id( $id_or_slug );
5925 $is_activated = self::has_instance( $addon_id );
5926
5927 if ( ! $is_activated ) {
5928 return false;
5929 }
5930
5931 if ( is_bool( $is_premium ) ) {
5932 // Check if the specified code version is activate.
5933 $addon = $this->get_addon_instance( $addon_id );
5934 $is_activated = ( $is_premium === $addon->is_premium() );
5935 }
5936
5937 return $is_activated;
5938 }
5939
5940 /**
5941 * Check if add-on was connected to install
5942 *
5943 * @author Vova Feldman (@svovaf)
5944 * @since 1.1.7
5945 *
5946 * @param string|number $id_or_slug
5947 *
5948 * @return bool
5949 */
5950 function is_addon_connected( $id_or_slug ) {
5951 $this->_logger->entrance();
5952
5953 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5954
5955 $addon_id = self::get_module_id( $id_or_slug );
5956 $addon = $this->get_addon( $addon_id );
5957 $slug = $addon->slug;
5958 if ( ! isset( $sites[ $slug ] ) ) {
5959 return false;
5960 }
5961
5962 $site = $sites[ $slug ];
5963
5964 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5965
5966 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5967 // The given slug do NOT belong to any of the plugin's add-ons.
5968 return false;
5969 }
5970
5971 return ( is_object( $site ) &&
5972 is_numeric( $site->id ) &&
5973 is_numeric( $site->user_id ) &&
5974 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5975 );
5976 }
5977
5978 /**
5979 * Determines if add-on installed.
5980 *
5981 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5982 *
5983 * @author Vova Feldman (@svovaf)
5984 * @since 1.0.6
5985 *
5986 * @param string|number $id_or_slug
5987 *
5988 * @return bool
5989 */
5990 function is_addon_installed( $id_or_slug ) {
5991 $this->_logger->entrance();
5992
5993 $addon_id = self::get_module_id( $id_or_slug );
5994
5995 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5996 }
5997
5998 /**
5999 * Get add-on basename.
6000 *
6001 * @author Vova Feldman (@svovaf)
6002 * @since 1.0.6
6003 *
6004 * @param string|number $id_or_slug
6005 *
6006 * @return string
6007 */
6008 function get_addon_basename( $id_or_slug ) {
6009 $addon_id = self::get_module_id( $id_or_slug );
6010
6011 if ( $this->is_addon_activated( $addon_id ) ) {
6012 return self::instance( $addon_id )->get_plugin_basename();
6013 }
6014
6015 $addon = $this->get_addon( $addon_id );
6016 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
6017
6018 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
6019 return $premium_basename;
6020 }
6021
6022 $all_plugins = $this->get_all_plugins();
6023
6024 foreach ( $all_plugins as $basename => $data ) {
6025 if ( $addon->slug === $data['slug'] ||
6026 $addon->premium_slug === $data['slug']
6027 ) {
6028 return $basename;
6029 }
6030 }
6031
6032 $free_basename = "{$addon->slug}/{$addon->slug}.php";
6033
6034 return $free_basename;
6035 }
6036
6037 /**
6038 * Get installed add-ons instances.
6039 *
6040 * @author Vova Feldman (@svovaf)
6041 * @since 1.0.6
6042 *
6043 * @return Freemius[]
6044 */
6045 function get_installed_addons() {
6046 if ( $this->is_addon() ) {
6047 // Add-on cannot have add-ons.
6048 return array();
6049 }
6050
6051 $installed_addons = array();
6052
6053 foreach ( self::$_instances as $instance ) {
6054 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
6055 $installed_addons[] = $instance;
6056 }
6057 }
6058
6059 return $installed_addons;
6060 }
6061
6062 /**
6063 * Check if any add-ons of the plugin are installed.
6064 *
6065 * @author Leo Fajardo (@leorw)
6066 * @since 1.1.1
6067 *
6068 * @return bool
6069 */
6070 function has_installed_addons() {
6071 if ( ! $this->has_addons() ) {
6072 return false;
6073 }
6074
6075 foreach ( self::$_instances as $instance ) {
6076 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
6077 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
6078 return true;
6079 }
6080 }
6081 }
6082
6083 return false;
6084 }
6085
6086 /**
6087 * Tell Freemius that the current plugin is an add-on.
6088 *
6089 * @author Vova Feldman (@svovaf)
6090 * @since 1.0.6
6091 *
6092 * @param number $parent_plugin_id The parent plugin ID
6093 */
6094 function init_addon( $parent_plugin_id ) {
6095 $this->_plugin->parent_plugin_id = $parent_plugin_id;
6096 }
6097
6098 /**
6099 * @author Vova Feldman (@svovaf)
6100 * @since 1.0.6
6101 *
6102 * @return bool
6103 */
6104 function is_addon() {
6105 return (
6106 isset( $this->_plugin->parent_plugin_id ) &&
6107 is_numeric( $this->_plugin->parent_plugin_id )
6108 );
6109 }
6110
6111 /**
6112 * @author Vova Feldman (@svovaf)
6113 * @since 2.3.2
6114 *
6115 * @param number $parent_product_id
6116 *
6117 * @return bool
6118 */
6119 function is_addon_of( $parent_product_id ) {
6120 return (
6121 $this->is_addon() &&
6122 $parent_product_id == $this->_plugin->parent_plugin_id
6123 );
6124 }
6125
6126 /**
6127 * Deactivate add-on if it's premium only and the user does't have a valid license.
6128 *
6129 * @param bool $is_after_trial_cancel
6130 *
6131 * @return bool If add-on was deactivated.
6132 */
6133 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6134 if ( ! $this->has_free_plan() &&
6135 ! $this->has_features_enabled_license() &&
6136 ! $this->_has_premium_license()
6137 ) {
6138 if ( $this->is_registered() ) {
6139 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6140 // if (empty($this->_storage->activation_timestamp) ||
6141 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6142 // ) {
6143 /**
6144 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6145 *
6146 * Retry syncing the user add-on licenses.
6147 */
6148 // Sync licenses.
6149 $this->_sync_licenses();
6150 // }
6151
6152 // Try to activate premium license.
6153 $this->_activate_license( true );
6154 }
6155
6156 if ( ! $this->has_free_plan() &&
6157 ! $this->has_features_enabled_license() &&
6158 ! $this->_has_premium_license()
6159 ) {
6160 // @todo Check if deactivate plugins also call the deactivation hook.
6161
6162 $this->_parent->_admin_notices->add_sticky(
6163 sprintf(
6164 ( $is_after_trial_cancel ?
6165 $this->_parent->get_text_inline(
6166 '%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.',
6167 'addon-trial-cancelled-message'
6168 ) :
6169 $this->_parent->get_text_inline(
6170 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6171 'addon-no-license-message'
6172 )
6173 ),
6174 '<b>' . $this->_plugin->title . '</b>'
6175 ) . ' ' . sprintf(
6176 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6177 $this->_parent->addon_url( $this->_slug ),
6178 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6179 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6180 ),
6181 'no_addon_license_' . $this->_slug,
6182 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6183 ( $is_after_trial_cancel ? 'success' : 'error' )
6184 );
6185
6186 deactivate_plugins( array( $this->_plugin_basename ), true );
6187
6188 return true;
6189 }
6190 }
6191
6192 return false;
6193 }
6194
6195 #endregion
6196
6197 #----------------------------------------------------------------------------------
6198 #region Sandbox
6199 #----------------------------------------------------------------------------------
6200
6201 /**
6202 * Set Freemius into sandbox mode for debugging.
6203 *
6204 * @author Vova Feldman (@svovaf)
6205 * @since 1.0.4
6206 *
6207 * @param string $secret_key
6208 */
6209 function init_sandbox( $secret_key ) {
6210 $this->_plugin->secret_key = $secret_key;
6211
6212 // Update plugin details.
6213 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6214 }
6215
6216 /**
6217 * Check if running payments in sandbox mode.
6218 *
6219 * @author Vova Feldman (@svovaf)
6220 * @since 1.0.4
6221 *
6222 * @return bool
6223 */
6224 function is_payments_sandbox() {
6225 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6226 }
6227
6228 #endregion
6229
6230 /**
6231 * Check if running test vs. live plugin.
6232 *
6233 * @author Vova Feldman (@svovaf)
6234 * @since 1.0.5
6235 *
6236 * @return bool
6237 */
6238 function is_live() {
6239 return $this->_plugin->is_live;
6240 }
6241
6242 /**
6243 * Check if super-admin skipped connection for all sites in the network.
6244 *
6245 * @author Vova Feldman (@svovaf)
6246 * @since 2.0.0
6247 */
6248 function is_network_anonymous() {
6249 if ( ! $this->_is_network_active ) {
6250 return false;
6251 }
6252
6253 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6254
6255 if ( empty( $is_anonymous_ms ) ) {
6256 return false;
6257 }
6258
6259 return $is_anonymous_ms['is'];
6260 }
6261
6262 /**
6263 * Check if super-admin opted-in for all sites in the network.
6264 *
6265 * @author Vova Feldman (@svovaf)
6266 * @since 2.0.0
6267 */
6268 function is_network_connected() {
6269 if ( ! $this->_is_network_active ) {
6270 return false;
6271 }
6272
6273 return $this->_storage->get( 'is_network_connected' );
6274 }
6275
6276 /**
6277 * Check if the user skipped connecting the account with Freemius.
6278 *
6279 * @author Vova Feldman (@svovaf)
6280 * @since 1.0.7
6281 *
6282 * @return bool
6283 */
6284 function is_anonymous() {
6285 if ( ! isset( $this->_is_anonymous ) ) {
6286 if ( $this->is_network_anonymous() ) {
6287 $this->_is_anonymous = true;
6288 } else if ( ! fs_is_network_admin() ) {
6289 if ( ! isset( $this->_storage->is_anonymous ) ) {
6290 // Not skipped.
6291 $this->_is_anonymous = false;
6292 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6293 // For back compatibility, since the variable was boolean before.
6294 $this->_is_anonymous = $this->_storage->is_anonymous;
6295
6296 // Upgrade stored data format to 1.1.3 format.
6297 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6298 } else {
6299 // Version 1.1.3 and later.
6300 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6301 }
6302 }
6303 }
6304
6305 return $this->_is_anonymous;
6306 }
6307
6308 /**
6309 * Check if the user skipped the connection of a specified site.
6310 *
6311 * @author Vova Feldman (@svovaf)
6312 * @since 2.0.0
6313 *
6314 * @param int $blog_id
6315 *
6316 * @return bool
6317 */
6318 function is_anonymous_site( $blog_id = 0 ) {
6319 if ( $this->is_network_anonymous() ) {
6320 return true;
6321 }
6322
6323 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6324
6325 if ( empty( $is_anonymous ) ) {
6326 return false;
6327 }
6328
6329 return $is_anonymous['is'];
6330 }
6331
6332 /**
6333 * Check if user connected his account and install pending email activation.
6334 *
6335 * @author Vova Feldman (@svovaf)
6336 * @since 1.0.7
6337 *
6338 * @return bool
6339 */
6340 function is_pending_activation() {
6341 return $this->_storage->get( 'is_pending_activation', false );
6342 }
6343
6344 /**
6345 * Check if plugin must be WordPress.org compliant.
6346 *
6347 * @since 1.0.7
6348 *
6349 * @return bool
6350 */
6351 function is_org_repo_compliant() {
6352 return $this->_is_org_compliant;
6353 }
6354
6355 #--------------------------------------------------------------------------------
6356 #region WP Cron Common
6357 #--------------------------------------------------------------------------------
6358
6359 /**
6360 * @author Vova Feldman (@svovaf)
6361 * @since 2.0.0
6362 *
6363 * @param string $name Cron name.
6364 *
6365 * @return object
6366 */
6367 private function get_cron_data( $name ) {
6368 $this->_logger->entrance( $name );
6369
6370 /**
6371 * @var object $cron_data
6372 */
6373 return $this->_storage->get( "{$name}_cron", null );
6374 }
6375
6376 /**
6377 * @author Vova Feldman (@svovaf)
6378 * @since 2.0.0
6379 *
6380 * @param string $name Cron name.
6381 */
6382 private function clear_cron_data( $name ) {
6383 $this->_logger->entrance( $name );
6384
6385 $this->_storage->remove( "{$name}_cron" );
6386 }
6387
6388 /**
6389 * @author Vova Feldman (@svovaf)
6390 * @since 2.0.0
6391 *
6392 * @param string $name Cron name.
6393 * @param int $cron_blog_id The cron executing blog ID.
6394 */
6395 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6396 $this->_logger->entrance( $name );
6397
6398 $this->_storage->store( "{$name}_cron", (object) array(
6399 'version' => $this->get_plugin_version(),
6400 'blog_id' => $cron_blog_id,
6401 'sdk_version' => $this->version,
6402 'timestamp' => WP_FS__SCRIPT_START_TIME,
6403 'on' => true,
6404 ) );
6405 }
6406
6407 /**
6408 * Get the cron's executing blog ID.
6409 *
6410 * @author Vova Feldman (@svovaf)
6411 * @since 2.0.0
6412 *
6413 * @param string $name Cron name.
6414 *
6415 * @return int
6416 */
6417 private function get_cron_blog_id( $name ) {
6418 $this->_logger->entrance( $name );
6419
6420 /**
6421 * @var object $cron_data
6422 */
6423 $cron_data = $this->get_cron_data( $name );
6424
6425 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6426 $cron_data->blog_id :
6427 0;
6428 }
6429
6430 /**
6431 * @author Vova Feldman (@svovaf)
6432 * @since 2.0.0
6433 *
6434 * @param string $name Cron name.
6435 *
6436 * @return bool
6437 */
6438 private function is_cron_on( $name ) {
6439 $this->_logger->entrance( $name );
6440
6441 /**
6442 * @var object $cron_data
6443 */
6444 $cron_data = $this->get_cron_data( $name );
6445
6446 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6447 }
6448
6449 /**
6450 * Unix timestamp for previous cron execution or false if never executed.
6451 *
6452 * @author Vova Feldman (@svovaf)
6453 * @since 2.0.0
6454 *
6455 * @param string $name Cron name.
6456 *
6457 * @return int|false
6458 */
6459 private function cron_last_execution( $name ) {
6460 $this->_logger->entrance( $name );
6461
6462 return $this->_storage->get( "{$name}_timestamp" );
6463 }
6464
6465 /**
6466 * Set cron execution time to now.
6467 *
6468 * @author Vova Feldman (@svovaf)
6469 * @since 2.0.0
6470 *
6471 * @param string $name Cron name.
6472 */
6473 private function set_cron_execution_timestamp( $name ) {
6474 $this->_logger->entrance( $name );
6475
6476 $this->_storage->store( "{$name}_timestamp", time() );
6477 }
6478
6479 /**
6480 * Sets the keepalive time to now.
6481 *
6482 * @author Leo Fajardo (@leorw)
6483 * @since 2.2.3
6484 *
6485 * @param bool|null $use_network_level_storage
6486 */
6487 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6488 $this->_logger->entrance();
6489
6490 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6491 }
6492
6493 /**
6494 * Check if cron was executed in the last $period of seconds.
6495 *
6496 * @author Vova Feldman (@svovaf)
6497 * @since 2.0.0
6498 *
6499 * @param string $name Cron name.
6500 * @param int $period In seconds
6501 *
6502 * @return bool
6503 */
6504 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6505 $this->_logger->entrance( $name );
6506
6507 $last_execution = $this->cron_last_execution( $name );
6508
6509 if ( ! is_numeric( $last_execution ) ) {
6510 return false;
6511 }
6512
6513 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6514 }
6515
6516 /**
6517 * WP Cron is executed on a site level. When running in a multisite network environment
6518 * with the network integration activated, for optimization reasons, we are consolidating
6519 * the installs data sync cron to be executed only from a single site.
6520 *
6521 * @author Vova Feldman (@svovaf)
6522 * @since 2.0.0
6523 *
6524 * @param int $except_blog_id Target any except the excluded blog ID.
6525 *
6526 * @return int
6527 */
6528 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6529 if ( ! is_multisite() ) {
6530 return 0;
6531 }
6532
6533 if ( $this->_is_network_active &&
6534 is_numeric( $this->_storage->network_install_blog_id ) &&
6535 $except_blog_id != $this->_storage->network_install_blog_id &&
6536 self::is_site_active( $this->_storage->network_install_blog_id )
6537 ) {
6538 // Try to run cron from the main network blog.
6539 $install = $this->get_install_by_blog_id( $this->_storage->network_install_blog_id );
6540
6541 if ( is_object( $install ) &&
6542 ( $this->is_premium() || $install->is_tracking_allowed() )
6543 ) {
6544 return $this->_storage->network_install_blog_id;
6545 }
6546 }
6547
6548 // Get first opted-in blog ID with active tracking.
6549 $installs = $this->get_blog_install_map();
6550 foreach ( $installs as $blog_id => $install ) {
6551 if ( $except_blog_id != $blog_id &&
6552 self::is_site_active( $blog_id ) &&
6553 ( $this->is_premium() || $install->is_tracking_allowed() )
6554 ) {
6555 return $blog_id;
6556 }
6557 }
6558
6559 return 0;
6560 }
6561
6562 /**
6563 * @author Vova Feldman (@svovaf)
6564 * @since 2.0.0
6565 *
6566 * @param string $name Cron name.
6567 * @param string $action_tag Callback action tag.
6568 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6569 */
6570 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6571 $this->_logger->entrance( $name );
6572
6573 if ( ! $this->is_cron_on( $name ) ) {
6574 return;
6575 }
6576
6577 $clear_cron = true;
6578 if ( ! $is_network_clear && $this->_is_network_active ) {
6579 $installs = $this->get_blog_install_map();
6580
6581 foreach ( $installs as $blog_id => $install ) {
6582 /**
6583 * @var FS_Site $install
6584 */
6585 if ( $install->is_tracking_allowed() ) {
6586 $clear_cron = false;
6587 break;
6588 }
6589 }
6590 }
6591
6592 if ( ! $clear_cron ) {
6593 return;
6594 }
6595
6596 /**
6597 * @var object $cron_data
6598 */
6599 $cron_data = $this->get_cron_data( $name );
6600
6601 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6602 $cron_data->blog_id :
6603 0;
6604
6605 $this->clear_cron_data( $name );
6606
6607 if ( 0 < $cron_blog_id ) {
6608 switch_to_blog( $cron_blog_id );
6609 }
6610
6611 if ( empty( $action_tag ) ) {
6612 $action_tag = $name;
6613 }
6614
6615 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6616
6617 if ( 0 < $cron_blog_id ) {
6618 restore_current_blog();
6619 }
6620 }
6621
6622 /**
6623 * Unix timestamp for next cron execution or false if not scheduled.
6624 *
6625 * @author Vova Feldman (@svovaf)
6626 * @since 2.0.0
6627 *
6628 * @param string $name Cron name.
6629 * @param string $action_tag Callback action tag.
6630 *
6631 * @return int|false
6632 */
6633 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6634 $this->_logger->entrance( $name );
6635
6636 if ( ! $this->is_cron_on( $name ) ) {
6637 return false;
6638 }
6639
6640 /**
6641 * @var object $cron_data
6642 */
6643 $cron_data = $this->get_cron_data( $name );
6644
6645 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6646 $cron_data->blog_id :
6647 0;
6648
6649 if ( 0 < $cron_blog_id ) {
6650 switch_to_blog( $cron_blog_id );
6651 }
6652
6653 if ( empty( $action_tag ) ) {
6654 $action_tag = $name;
6655 }
6656
6657 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6658
6659 if ( 0 < $cron_blog_id ) {
6660 restore_current_blog();
6661 }
6662
6663 return $next_scheduled;
6664 }
6665
6666 /**
6667 * @author Vova Feldman (@svovaf)
6668 * @since 2.0.0
6669 *
6670 * @param string $name Cron name.
6671 * @param string $action_tag Callback action tag.
6672 * @param string $recurrence 'single' or 'daily'.
6673 * @param int $start_at Defaults to now.
6674 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6675 * @param int $except_blog_id Target any except the excluded blog ID.
6676 */
6677 private function schedule_cron(
6678 $name,
6679 $action_tag = '',
6680 $recurrence = 'single',
6681 $start_at = WP_FS__SCRIPT_START_TIME,
6682 $randomize_start = true,
6683 $except_blog_id = 0
6684 ) {
6685 $this->_logger->entrance( $name );
6686
6687 $this->clear_cron( $name, $action_tag, true );
6688
6689 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6690
6691 if ( is_multisite() && 0 == $cron_blog_id ) {
6692 // Don't schedule cron since couldn't find a target blog.
6693 return;
6694 }
6695
6696 if ( 0 < $cron_blog_id ) {
6697 switch_to_blog( $cron_blog_id );
6698 }
6699
6700 if ( 'daily' === $recurrence ) {
6701 if ( $randomize_start ) {
6702 // Schedule first sync with a random 12 hour time range from now.
6703 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6704 }
6705
6706 // Schedule daily WP cron.
6707 wp_schedule_event(
6708 $start_at,
6709 'daily',
6710 $this->get_action_tag( $action_tag )
6711 );
6712 } else if ( 'single' === $recurrence ) {
6713 // Schedule single cron.
6714 wp_schedule_single_event(
6715 $start_at,
6716 $this->get_action_tag( $action_tag )
6717 );
6718 }
6719
6720 $this->set_cron_data( $name, $cron_blog_id );
6721
6722 if ( 0 < $cron_blog_id ) {
6723 restore_current_blog();
6724 }
6725 }
6726
6727 /**
6728 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6729 * that doesn't halt page loading.
6730 *
6731 * @author Vova Feldman (@svovaf)
6732 * @since 2.0.0
6733 *
6734 * @param string $name Cron name.
6735 * @param callable $callable The function that should be executed.
6736 */
6737 private function execute_cron( $name, $callable ) {
6738 $this->_logger->entrance( $name );
6739
6740 // Store the last time data sync was executed.
6741 $this->set_cron_execution_timestamp( $name );
6742
6743 // Check if API is temporary down.
6744 if ( FS_Api::is_temporary_down() ) {
6745 return;
6746 }
6747
6748 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6749
6750 $users_2_blog_ids = array();
6751
6752 if ( ! is_multisite() ) {
6753 // Add dummy blog.
6754 $users_2_blog_ids[0] = array( 0 );
6755 } else {
6756 $installs = $this->get_blog_install_map();
6757 foreach ( $installs as $blog_id => $install ) {
6758 if ( $this->is_premium() || $install->is_tracking_allowed() ) {
6759 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6760 $users_2_blog_ids[ $install->user_id ] = array();
6761 }
6762
6763 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6764 }
6765 }
6766 }
6767
6768 $current_blog_id = get_current_blog_id();
6769
6770 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6771 if ( 0 < $blog_ids[0] ) {
6772 $this->switch_to_blog( $blog_ids[0] );
6773 }
6774
6775 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6776
6777 foreach ( $blog_ids as $blog_id ) {
6778 $this->do_action( "after_{$name}_cron", $blog_id );
6779 }
6780 }
6781
6782 if ( is_multisite() ) {
6783 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6784
6785 $this->do_action( "after_{$name}_cron_multisite" );
6786 }
6787 }
6788
6789 #endregion
6790
6791 #----------------------------------------------------------------------------------
6792 #region Daily Sync Cron
6793 #----------------------------------------------------------------------------------
6794
6795
6796 /**
6797 * @author Vova Feldman (@svovaf)
6798 * @since 2.0.0
6799 *
6800 * @return bool
6801 */
6802 private function is_sync_cron_scheduled() {
6803 return $this->is_cron_on( 'sync' );
6804 }
6805
6806 /**
6807 * Get the sync cron's executing blog ID.
6808 *
6809 * @author Vova Feldman (@svovaf)
6810 * @since 2.0.0
6811 *
6812 * @return int
6813 */
6814 private function get_sync_cron_blog_id() {
6815 return $this->get_cron_blog_id( 'sync' );
6816 }
6817
6818 /**
6819 * @author Vova Feldman (@svovaf)
6820 * @since 1.1.7.3
6821 */
6822 private function run_manual_sync() {
6823 self::require_pluggable_essentials();
6824
6825 if ( ! $this->is_user_admin() ) {
6826 return;
6827 }
6828
6829 // Run manual sync.
6830 $this->_sync_cron();
6831
6832 // Reschedule next cron to run 24 hours from now (performance optimization).
6833 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6834 }
6835
6836 /**
6837 * Data sync cron job. Replaces the background sync non blocking HTTP request
6838 * that doesn't halt page loading.
6839 *
6840 * @author Vova Feldman (@svovaf)
6841 * @since 1.1.7.3
6842 * @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.
6843 */
6844 function _sync_cron() {
6845 $this->_logger->entrance();
6846
6847 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6848 }
6849
6850 /**
6851 * The actual data sync cron logic.
6852 *
6853 * @author Vova Feldman (@svovaf)
6854 * @since 2.0.0
6855 *
6856 * @param int[] $blog_ids
6857 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6858 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6859 * updates for a single site in case `execute_cron` has switched to a different blog.
6860 */
6861 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6862 if ( $this->is_registered() ) {
6863 $this->sync_user_beta_mode();
6864
6865 if ( $this->has_paid_plan() ) {
6866 // Initiate background plan sync.
6867 $this->_sync_license( true, false, $current_blog_id );
6868
6869 if ( $this->is_paying() ) {
6870 // Check for premium plugin updates.
6871 $this->check_updates( true );
6872 }
6873 } else {
6874 // Sync install(s) (only if something changed locally).
6875 if ( 1 < count( $blog_ids ) ) {
6876 $this->sync_installs();
6877 } else {
6878 $this->sync_install();
6879 }
6880
6881 $this->maybe_sync_install_user();
6882 }
6883 }
6884 }
6885
6886 /**
6887 * Check if sync was executed in the last $period of seconds.
6888 *
6889 * @author Vova Feldman (@svovaf)
6890 * @since 1.1.7.3
6891 *
6892 * @param int $period In seconds
6893 *
6894 * @return bool
6895 */
6896 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6897 return $this->is_cron_executed( 'sync', $period );
6898 }
6899
6900 /**
6901 * @author Vova Feldman (@svovaf)
6902 * @since 1.1.7.3
6903 *
6904 * @return bool
6905 */
6906 private function is_sync_cron_on() {
6907 return $this->is_cron_on( 'sync' );
6908 }
6909
6910 /**
6911 * @author Vova Feldman (@svovaf)
6912 * @since 1.1.7.3
6913 *
6914 * @param int $start_at Defaults to now.
6915 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6916 * @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.
6917 */
6918 private function schedule_sync_cron(
6919 $start_at = WP_FS__SCRIPT_START_TIME,
6920 $randomize_start = true,
6921 $except_blog_id = 0
6922 ) {
6923 $this->schedule_cron(
6924 'sync',
6925 'data_sync',
6926 'daily',
6927 $start_at,
6928 $randomize_start,
6929 $except_blog_id
6930 );
6931 }
6932
6933 /**
6934 * Add the actual sync function to the cron job hook.
6935 *
6936 * @author Vova Feldman (@svovaf)
6937 * @since 1.1.7.3
6938 */
6939 private function hook_callback_to_sync_cron() {
6940 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6941 }
6942
6943 /**
6944 * @author Vova Feldman (@svovaf)
6945 * @since 1.1.7.3
6946 *
6947 * @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.
6948 */
6949 private function clear_sync_cron( $is_network_clear = false ) {
6950 $this->_logger->entrance();
6951
6952 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6953 }
6954
6955 /**
6956 * Unix timestamp for next sync cron execution or false if not scheduled.
6957 *
6958 * @author Vova Feldman (@svovaf)
6959 * @since 1.1.7.3
6960 *
6961 * @return int|false
6962 */
6963 function next_sync_cron() {
6964 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6965 }
6966
6967 /**
6968 * Unix timestamp for previous sync cron execution or false if never executed.
6969 *
6970 * @author Vova Feldman (@svovaf)
6971 * @since 1.1.7.3
6972 *
6973 * @return int|false
6974 */
6975 function last_sync_cron() {
6976 return $this->cron_last_execution( 'sync' );
6977 }
6978
6979 #endregion Daily Sync Cron ------------------------------------------------------------------
6980
6981 #----------------------------------------------------------------------------------
6982 #region Async Install Sync
6983 #----------------------------------------------------------------------------------
6984
6985 /**
6986 * @author Vova Feldman (@svovaf)
6987 * @since 1.1.7.3
6988 *
6989 * @return bool
6990 */
6991 private function is_install_sync_scheduled() {
6992 return $this->is_cron_on( 'install_sync' );
6993 }
6994
6995 /**
6996 * Get the sync cron's executing blog ID.
6997 *
6998 * @author Vova Feldman (@svovaf)
6999 * @since 2.0.0
7000 *
7001 * @return int
7002 */
7003 private function get_install_sync_cron_blog_id() {
7004 return $this->get_cron_blog_id( 'install_sync' );
7005 }
7006
7007 /**
7008 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
7009 *
7010 * @author Vova Feldman (@svovaf)
7011 * @since 1.1.7.3
7012 *
7013 * @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.
7014 */
7015 private function schedule_install_sync( $except_blog_id = 0 ) {
7016 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
7017 }
7018
7019 /**
7020 * Unix timestamp for previous install sync cron execution or false if never executed.
7021 *
7022 * @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.
7023 *
7024 * @author Vova Feldman (@svovaf)
7025 * @since 1.1.7.3
7026 *
7027 * @return int|false
7028 */
7029 function last_install_sync() {
7030 return $this->cron_last_execution( 'install_sync' );
7031 }
7032
7033 /**
7034 * Unix timestamp for next install sync cron execution or false if not scheduled.
7035 *
7036 * @author Vova Feldman (@svovaf)
7037 * @since 1.1.7.3
7038 *
7039 * @return int|false
7040 */
7041 function next_install_sync() {
7042 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
7043 }
7044
7045 /**
7046 * Add the actual install sync function to the cron job hook.
7047 *
7048 * @author Vova Feldman (@svovaf)
7049 * @since 1.1.7.3
7050 */
7051 private function hook_callback_to_install_sync() {
7052 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
7053 }
7054
7055 /**
7056 * @author Vova Feldman (@svovaf)
7057 * @since 1.1.7.3
7058 *
7059 * @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.
7060 */
7061 private function clear_install_sync_cron( $is_network_clear = false ) {
7062 $this->_logger->entrance();
7063
7064 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
7065 }
7066
7067 /**
7068 * @author Vova Feldman (@svovaf)
7069 * @since 1.1.7.3
7070 * @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.
7071 */
7072 public function _run_sync_install() {
7073 $this->_logger->entrance();
7074
7075 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
7076 }
7077
7078 /**
7079 * The actual install(s) sync cron logic.
7080 *
7081 * @author Vova Feldman (@svovaf)
7082 * @since 2.0.0
7083 *
7084 * @param int[] $blog_ids
7085 * @param int|null $current_blog_id
7086 */
7087 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
7088 if ( $this->is_registered() ) {
7089 if ( 1 < count( $blog_ids ) ) {
7090 $this->sync_installs( array(), true );
7091 } else {
7092 $this->sync_install( array(), true );
7093 }
7094
7095 $this->maybe_sync_install_user();
7096 }
7097 }
7098
7099 #endregion Async Install Sync ------------------------------------------------------------------
7100
7101 /**
7102 * Show a notice that activation is currently pending.
7103 *
7104 * @author Vova Feldman (@svovaf)
7105 * @since 1.0.7
7106 *
7107 * @param bool|string $email
7108 * @param bool $is_pending_trial Since 1.2.1.5
7109 */
7110 function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) {
7111 if ( ! is_string( $email ) ) {
7112 $current_user = self::_get_current_wp_user();
7113 $email = $current_user->user_email;
7114 }
7115
7116 $this->_admin_notices->add_sticky(
7117 sprintf(
7118 $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' ),
7119 '<b>' . $this->get_plugin_name() . '</b>',
7120 '<b>' . $email . '</b>',
7121 ( $is_pending_trial ?
7122 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7123 $this->get_text_inline( 'complete the install', 'complete-the-install' ) )
7124 ),
7125 'activation_pending',
7126 'Thanks!'
7127 );
7128 }
7129
7130 /**
7131 * Check if currently in plugin activation.
7132 *
7133 * @author Vova Feldman (@svovaf)
7134 * @since 1.1.4
7135 *
7136 * @return bool
7137 */
7138 function is_plugin_activation() {
7139 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7140
7141 return !empty($result);
7142 }
7143
7144 /**
7145 *
7146 * NOTE: admin_menu action executed before admin_init.
7147 *
7148 * @author Vova Feldman (@svovaf)
7149 * @since 1.0.7
7150 */
7151 function _admin_init_action() {
7152 $is_migration = $this->is_migration();
7153
7154 /**
7155 * Automatically redirect to connect/activation page after plugin activation.
7156 *
7157 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7158 */
7159 if ( $this->is_plugin_activation() ) {
7160 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7161
7162 if ( isset( $_GET['activate-multi'] ) ) {
7163 /**
7164 * Don't redirect if activating multiple plugins at once (bulk activation).
7165 */
7166 } else if ( ! $is_migration ) {
7167 $this->_redirect_on_activation_hook();
7168 return;
7169 }
7170 }
7171
7172 if ( $is_migration ) {
7173 return;
7174 }
7175
7176 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7177 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7178
7179 $this->skip_connection( null, fs_is_network_admin() );
7180
7181 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7182 }
7183
7184 if ( $this->is_network_activation_mode() &&
7185 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7186 ) {
7187 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7188
7189 $this->delegate_connection();
7190
7191 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7192 }
7193
7194 $this->_add_upgrade_action_link();
7195
7196 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7197 (
7198 ( true === $this->_storage->require_license_activation ) ||
7199 // Not registered nor anonymous.
7200 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7201 // OR, network level and in network upgrade mode.
7202 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7203 )
7204 ) {
7205 if ( ! $this->is_pending_activation() ) {
7206 if ( ! $this->is_activation_page() ) {
7207 /**
7208 * If a user visits any other admin page before activating the premium-only theme with a valid
7209 * license, reactivate the previous theme.
7210 *
7211 * @author Leo Fajardo (@leorw)
7212 * @since 1.2.2
7213 */
7214 if ( $this->is_theme() &&
7215 ! $this->has_settings_menu() &&
7216 ! isset( $_REQUEST['fs_action'] ) &&
7217 $this->can_activate_previous_theme()
7218 ) {
7219 if ( $this->is_only_premium() ) {
7220 $this->activate_previous_theme();
7221 return;
7222 }
7223
7224 if ( true === $this->_storage->require_license_activation ) {
7225 $this->_storage->require_license_activation = false;
7226 }
7227 }
7228
7229 if ( ! fs_is_network_admin() &&
7230 $this->is_network_activation_mode() &&
7231 ! $this->is_delegated_connection()
7232 ) {
7233 return;
7234 }
7235
7236 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7237 if ( ! $this->_anonymous_mode ) {
7238 // Show notice for new plugin installations.
7239 $this->_admin_notices->add(
7240 sprintf(
7241 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7242 sprintf( '<b><a href="%s">%s</a></b>',
7243 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7244 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7245 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7246 )
7247 ),
7248 '',
7249 'update-nag'
7250 );
7251 }
7252 } else {
7253 if ( $this->should_add_sticky_optin_notice() ) {
7254 $this->add_sticky_optin_admin_notice();
7255 }
7256
7257 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7258 // Don't show admin nag if plugin update.
7259 wp_enqueue_script( 'wp-pointer' );
7260 wp_enqueue_style( 'wp-pointer' );
7261
7262 $this->_enqueue_connect_essentials();
7263
7264 add_action( 'admin_print_footer_scripts', array(
7265 $this,
7266 '_add_connect_pointer_script'
7267 ) );
7268 }
7269 }
7270 }
7271 }
7272
7273 if ( $this->show_opt_in_on_themes_page() &&
7274 $this->is_activation_page()
7275 ) {
7276 $this->_show_theme_activation_optin_dialog();
7277 }
7278 }
7279 }
7280
7281 /**
7282 * @author Vova Feldman (@svovaf)
7283 * @since 2.0.0
7284 *
7285 * @return bool
7286 */
7287 private function should_add_sticky_optin_notice() {
7288 if ( fs_is_network_admin() ) {
7289 if ( ! $this->_is_network_active ) {
7290 return false;
7291 }
7292
7293 if ( ! $this->is_network_activation_mode() ) {
7294 return false;
7295 }
7296
7297 return ! isset( $this->_storage->sticky_optin_added_ms );
7298 }
7299
7300 if ( ! $this->is_activation_mode() ) {
7301 return false;
7302 }
7303
7304 // If running from a blog admin and delegated the connection.
7305 return ! isset( $this->_storage->sticky_optin_added );
7306 }
7307
7308 /**
7309 * @author Leo Fajardo (@leorw)
7310 * @since 2.0.0
7311 */
7312 private function add_sticky_optin_admin_notice() {
7313 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7314 $this->_storage->sticky_optin_added = true;
7315 } else {
7316 $this->_storage->sticky_optin_added_ms = true;
7317 }
7318
7319 // Show notice for new plugin installations.
7320 $this->_admin_notices->add_sticky(
7321 sprintf(
7322 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7323 $this->_module_type,
7324 sprintf( '<b><a href="%s">%s</a></b>',
7325 $this->get_activation_url(),
7326 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7327 )
7328 ),
7329 'connect_account',
7330 '',
7331 'update-nag'
7332 );
7333 }
7334
7335 /**
7336 * Enqueue connect requires scripts and styles.
7337 *
7338 * @author Vova Feldman (@svovaf)
7339 * @since 1.1.4
7340 */
7341 function _enqueue_connect_essentials() {
7342 wp_enqueue_script( 'jquery' );
7343 wp_enqueue_script( 'json2' );
7344
7345 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7346 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7347
7348 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
7349 }
7350
7351 /**
7352 * Add connect / opt-in pointer.
7353 *
7354 * @author Vova Feldman (@svovaf)
7355 * @since 1.1.4
7356 */
7357 function _add_connect_pointer_script() {
7358 $vars = array( 'id' => $this->_module_id );
7359 $pointer_content = fs_get_template( 'connect.php', $vars );
7360 ?>
7361 <script type="text/javascript">// <![CDATA[
7362 jQuery(document).ready(function ($) {
7363 if ('undefined' !== typeof(jQuery().pointer)) {
7364
7365 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7366
7367 if (element.length > 0) {
7368 var optin = $(element).pointer($.extend(true, {}, {
7369 content : <?php echo json_encode( $pointer_content ) ?>,
7370 position : {
7371 edge : 'left',
7372 align: 'center'
7373 },
7374 buttons : function () {
7375 // Don't show pointer buttons.
7376 return '';
7377 },
7378 pointerWidth: 482
7379 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7380
7381 <?php
7382 echo $this->apply_filters( 'optin_pointer_execute', "
7383
7384 optin.pointer('open');
7385
7386 // Tag the opt-in pointer with custom class.
7387 $('.wp-pointer #fs_connect')
7388 .parents('.wp-pointer.wp-pointer-top')
7389 .addClass('fs-opt-in-pointer');
7390
7391 ", 'element', 'optin' ) ?>
7392 }
7393 }
7394 });
7395 // ]]></script>
7396 <?php
7397 }
7398
7399 /**
7400 * Return current page's URL.
7401 *
7402 * @author Vova Feldman (@svovaf)
7403 * @since 1.0.7
7404 *
7405 * @return string
7406 */
7407 function current_page_url() {
7408 $url = 'http';
7409
7410 if ( isset( $_SERVER["HTTPS"] ) ) {
7411 if ( $_SERVER["HTTPS"] == "on" ) {
7412 $url .= "s";
7413 }
7414 }
7415 $url .= "://";
7416 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7417 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7418 } else {
7419 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7420 }
7421
7422 return esc_url( $url );
7423 }
7424
7425 /**
7426 * Check if the current page is the plugin's main admin settings page.
7427 *
7428 * @author Vova Feldman (@svovaf)
7429 * @since 1.0.7
7430 *
7431 * @return bool
7432 */
7433 function _is_plugin_page() {
7434 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7435 fs_is_plugin_page( $this->_slug );
7436 }
7437
7438 /* Events
7439 ------------------------------------------------------------------------------------------------------------------*/
7440 /**
7441 * Delete site install from Database.
7442 *
7443 * @author Vova Feldman (@svovaf)
7444 * @since 1.0.1
7445 *
7446 * @param bool $store
7447 * @param int|null $blog_id Since 2.0.0
7448 *
7449 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7450 */
7451 function _delete_site( $store = true, $blog_id = null ) {
7452 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7453 }
7454
7455 /**
7456 * Delete site install from Database.
7457 *
7458 * @author Vova Feldman (@svovaf)
7459 * @since 1.2.2.7
7460 *
7461 * @param string $slug
7462 * @param string $module_type
7463 * @param bool $store
7464 * @param int|null $blog_id Since 2.0.0
7465 *
7466 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7467 */
7468 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7469 $sites = self::get_all_sites( $module_type, $blog_id );
7470
7471 $install_id = false;
7472
7473 if ( isset( $sites[ $slug ] ) ) {
7474 if ( is_object( $sites[ $slug ] ) ) {
7475 $install_id = $sites[ $slug ]->id;
7476 }
7477
7478 unset( $sites[ $slug ] );
7479
7480 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7481 }
7482
7483 return $install_id;
7484 }
7485
7486 /**
7487 * Delete user.
7488 *
7489 * @author Vova Feldman (@svovaf)
7490 * @since 2.0.0
7491 *
7492 * @param number $user_id
7493 * @param bool $store
7494 *
7495 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7496 */
7497 private static function delete_user( $user_id, $store = true ) {
7498 $users = self::get_all_users();
7499
7500 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7501 return false;
7502 }
7503
7504 unset( $users[ $user_id ] );
7505
7506 self::$_accounts->set_option( 'users', $users, $store );
7507
7508 return $user_id;
7509 }
7510
7511 /**
7512 * Delete plugin's plans information.
7513 *
7514 * @param bool $store Flush to Database if true.
7515 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7516 *
7517 * @author Vova Feldman (@svovaf)
7518 * @since 1.0.9
7519 */
7520 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7521 $this->_logger->entrance();
7522
7523 $plans = self::get_all_plans( $this->_module_type );
7524
7525 $plans_to_keep = array();
7526
7527 if ( $keep_associated_plans ) {
7528 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7529 foreach ( $plans_ids_to_keep as $plan_id ) {
7530 $plan = self::_get_plan_by_id( $plan_id );
7531 if ( is_object( $plan ) ) {
7532 $plans_to_keep[] = self::_encrypt_entity( $plan );
7533 }
7534 }
7535 }
7536
7537 if ( ! empty( $plans_to_keep ) ) {
7538 $plans[ $this->_slug ] = $plans_to_keep;
7539 } else {
7540 unset( $plans[ $this->_slug ] );
7541 }
7542
7543 $this->set_account_option( 'plans', $plans, $store );
7544 }
7545
7546 /**
7547 * Delete all plugin licenses.
7548 *
7549 * @author Vova Feldman (@svovaf)
7550 * @since 1.0.9
7551 *
7552 * @param bool $store
7553 */
7554 private function _delete_licenses( $store = true ) {
7555 $this->_logger->entrance();
7556
7557 $all_licenses = self::get_all_licenses();
7558
7559 unset( $all_licenses[ $this->_module_id ] );
7560
7561 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7562 }
7563
7564 /**
7565 * Check if Freemius was added on new plugin installation.
7566 *
7567 * @author Vova Feldman (@svovaf)
7568 * @since 1.1.5
7569 *
7570 * @return bool
7571 */
7572 function is_plugin_new_install() {
7573 return isset( $this->_storage->is_plugin_new_install ) &&
7574 $this->_storage->is_plugin_new_install;
7575 }
7576
7577 /**
7578 * Check if it's the first plugin release that is running Freemius.
7579 *
7580 * @author Vova Feldman (@svovaf)
7581 * @since 1.2.1.5
7582 *
7583 * @return bool
7584 */
7585 function is_first_freemius_powered_version() {
7586 return empty( $this->_storage->plugin_last_version );
7587 }
7588
7589 /**
7590 * @author Leo Fajardo (@leorw)
7591 * @since 1.2.2
7592 *
7593 * @return bool|string
7594 */
7595 private function get_previous_theme_slug() {
7596 return isset( $this->_storage->previous_theme ) ?
7597 $this->_storage->previous_theme :
7598 false;
7599 }
7600
7601 /**
7602 * @author Leo Fajardo (@leorw)
7603 * @since 1.2.2
7604 *
7605 * @return string
7606 */
7607 private function can_activate_previous_theme() {
7608 $slug = $this->get_previous_theme_slug();
7609 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7610 $theme_instance = wp_get_theme( $slug );
7611
7612 return $theme_instance->exists();
7613 }
7614
7615 return false;
7616 }
7617
7618 /**
7619 * @author Leo Fajardo (@leorw)
7620 * @since 1.2.2
7621 */
7622 private function activate_previous_theme() {
7623 switch_theme( $this->get_previous_theme_slug() );
7624 unset( $this->_storage->previous_theme );
7625
7626 global $pagenow;
7627 if ( 'themes.php' === $pagenow ) {
7628 /**
7629 * Refresh the active theme information.
7630 *
7631 * @author Leo Fajardo (@leorw)
7632 * @since 1.2.2
7633 */
7634 fs_redirect( $this->admin_url( $pagenow ) );
7635 }
7636 }
7637
7638 /**
7639 * @author Leo Fajardo (@leorw)
7640 * @since 1.2.2
7641 *
7642 * @return string
7643 */
7644 function get_previous_theme_activation_url() {
7645 if ( ! $this->can_activate_previous_theme() ) {
7646 return '';
7647 }
7648
7649 /**
7650 * Activation URL
7651 *
7652 * @author Leo Fajardo (@leorw)
7653 * @since 1.2.2
7654 */
7655 return wp_nonce_url(
7656 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7657 'switch-theme_' . $this->get_previous_theme_slug()
7658 );
7659 }
7660
7661 /**
7662 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7663 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7664 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7665 * theme doesn't exist, then there will be no close button.
7666 *
7667 * @author Leo Fajardo (@leorw)
7668 * @since 1.2.2
7669 *
7670 * @param string $slug_or_name Old theme's slug or name.
7671 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7672 */
7673 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7674 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7675 $old_theme->get_stylesheet() :
7676 $slug_or_name;
7677
7678 $this->_activate_plugin_event_hook();
7679 }
7680
7681 /**
7682 * Plugin activated hook.
7683 *
7684 * @author Vova Feldman (@svovaf)
7685 * @since 1.0.1
7686 *
7687 * @uses FS_Api
7688 */
7689 function _activate_plugin_event_hook() {
7690 $this->_logger->entrance( 'slug = ' . $this->_slug );
7691
7692 if ( ! $this->is_user_admin() ) {
7693 return;
7694 }
7695
7696 $this->unregister_uninstall_hook();
7697
7698 // Clear API cache on activation.
7699 FS_Api::clear_cache();
7700
7701 $is_premium_version_activation = $this->is_plugin() ?
7702 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7703 $this->is_premium();
7704
7705 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7706
7707 if ( $this->is_plugin() ) {
7708 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7709 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7710 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7711 $other_version_basename = $is_premium_version_activation ?
7712 $this->_free_plugin_basename :
7713 $this->premium_plugin_basename();
7714
7715 if ( ! $this->_is_network_active ) {
7716 /**
7717 * Themes are always network activated, but the ACTUAL activation is per site.
7718 *
7719 * During the activation, the plugin isn't yet active, therefore,
7720 * _is_network_active will be set to false even if it's a network level
7721 * activation. So we need to fix that by looking at the is_network_admin() value.
7722 *
7723 * @author Vova Feldman
7724 */
7725 $this->_is_network_active = (
7726 $this->_is_multisite_integrated &&
7727 fs_is_network_admin()
7728 );
7729 }
7730
7731 /**
7732 * If the other module version is active, deactivate it.
7733 *
7734 * is_plugin_active() checks if the plugin is active on the site or the network level and
7735 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7736 *
7737 * @author Leo Fajardo (@leorw)
7738 * @since 1.2.2
7739 */
7740 if (
7741 is_plugin_active( $other_version_basename ) &&
7742 $this->apply_filters( 'deactivate_on_activation', true )
7743 ) {
7744 deactivate_plugins( $other_version_basename );
7745 }
7746 }
7747
7748 if ( $this->is_registered() ) {
7749 if ( $is_premium_version_activation ) {
7750 $this->reconnect_locally();
7751 }
7752
7753
7754 // Schedule re-activation event and sync.
7755 // $this->sync_install( array(), true );
7756 $this->schedule_install_sync();
7757
7758 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7759 if ( $is_premium_version_activation ) {
7760 $this->_admin_notices->add(
7761 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7762 $this->get_text_x_inline( 'W00t',
7763 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7764 );
7765 }
7766 } else if ( $this->is_anonymous() ) {
7767 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7768 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7769 $network = true;
7770 } else {
7771 $plugin_version = $this->_storage->is_anonymous['version'];
7772 $network = false;
7773 }
7774
7775 /**
7776 * Reset "skipped" click cache on the following:
7777 * 1. Freemius DEV mode.
7778 * 2. WordPress DEBUG mode.
7779 * 3. If a plugin and the user skipped the exact same version before.
7780 *
7781 * @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).
7782 *
7783 * @todo 4. If explicitly asked to retry after every activation.
7784 */
7785 if ( WP_FS__DEV_MODE ||
7786 (
7787 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7788 $this->get_plugin_version() == $plugin_version
7789 )
7790 ) {
7791 $this->reset_anonymous_mode( $network );
7792 }
7793 }
7794
7795 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7796
7797 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7798 /**
7799 * When activating an add-on, try to also activate a license.
7800 *
7801 * @author Leo Fajardo (@leorw)
7802 * @since 2.3.0
7803 */
7804 if ( ! $this->_is_network_active ) {
7805 $this->maybe_activate_addon_license();
7806 } else {
7807 $this->maybe_network_activate_addon_license();
7808 }
7809
7810 /**
7811 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7812 *
7813 * @author Leo Fajardo (@leorw)
7814 * @since 2.3.0
7815 */
7816 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7817
7818 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7819 $this->_storage->require_license_activation = false;
7820 }
7821 }
7822
7823 if (
7824 $is_premium_version_activation &&
7825 (
7826 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7827 (
7828 $this->is_registered() &&
7829 ! $is_trial_or_has_features_enabled_license
7830 )
7831 )
7832 ) {
7833 $this->_storage->require_license_activation = true;
7834 }
7835
7836 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7837 /**
7838 * If no previous version of plugin's version exist, it means that it's either
7839 * the first time that the plugin installed on the site, or the plugin was installed
7840 * before but didn't have Freemius integrated.
7841 *
7842 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7843 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7844 * only after immediate activation.
7845 *
7846 * @since 1.1.4
7847 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7848 */
7849 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7850 }
7851
7852 /**
7853 * Also flush when activating the premium version so that even if Freemius was off before, the API
7854 * connectivity test can be run again.
7855 *
7856 * @author Leo Fajardo (@leorw)
7857 * @since 2.2.3.1
7858 */
7859 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7860
7861 if ( ! $this->_anonymous_mode &&
7862 $has_api_connectivity &&
7863 ! $this->_isAutoInstall
7864 ) {
7865 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7866 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7867 }
7868
7869 /**
7870 * Activation hook is executed after the plugin's main file is loaded, therefore,
7871 * after the plugin was loaded. The logic is located at activate_plugin()
7872 * ./wp-admin/includes/plugin.php.
7873 *
7874 * @author Vova Feldman (@svovaf)
7875 * @since 1.1.9
7876 */
7877 $this->_storage->was_plugin_loaded = true;
7878 }
7879
7880 /**
7881 * @author Leo Fajardo (@leorw)
7882 * @since 2.3.0
7883 */
7884 private function maybe_activate_addon_license() {
7885 $parent_fs = $this->get_parent_instance();
7886
7887 if (
7888 ! is_object( $parent_fs ) ||
7889 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7890 ) {
7891 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7892 return;
7893 }
7894
7895 $license = $this->get_active_parent_license();
7896 if ( ! is_object( $license ) ) {
7897 return;
7898 }
7899
7900 if (
7901 $this->is_bundle_license_auto_activation_enabled() &&
7902 ! empty( $license->products )
7903 ) {
7904 $this->activate_bundle_license( $license );
7905
7906 return;
7907 }
7908
7909 if ( ! $this->is_registered() ) {
7910 // Opt in with a license key.
7911 $this->opt_in(
7912 $parent_fs->get_current_or_network_user()->email,
7913 false,
7914 false,
7915 $license->secret_key
7916 );
7917 } else {
7918 // Activate the license.
7919 $install = $this->get_api_site_scope()->call(
7920 '/',
7921 'put',
7922 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7923 );
7924
7925 if ( ! FS_Api::is_api_error( $install ) ) {
7926 $this->_sync_addon_license( $this->get_id(), true );
7927 }
7928 }
7929 }
7930
7931 /**
7932 * @author Leo Fajardo (@leorw)
7933 * @since 2.3.0
7934 *
7935 * @param FS_Plugin_License $license
7936 */
7937 private function maybe_network_activate_addon_license( $license = null ) {
7938 $parent_fs = $this->get_parent_instance();
7939 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7940 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7941 return;
7942 }
7943
7944 $license = ( ! is_null( $license ) ) ?
7945 $license :
7946 $this->get_active_parent_license();
7947
7948 if ( ! is_object( $license ) ) {
7949 return;
7950 }
7951
7952 if (
7953 $this->is_bundle_license_auto_activation_enabled() &&
7954 ! empty( $license->products )
7955 ) {
7956 $this->activate_bundle_license( $license );
7957
7958 return;
7959 }
7960
7961 if ( ! $this->is_network_registered() ) {
7962 $sites = $this->get_sites_for_network_level_optin();
7963
7964 if ( count( $sites ) > $license->left() ) {
7965 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7966 return;
7967 }
7968
7969 // Opt in with a license key.
7970 $this->opt_in(
7971 $parent_fs->get_user()->email,
7972 false,
7973 false,
7974 $license->secret_key,
7975 false,
7976 false,
7977 false,
7978 null,
7979 $sites
7980 );
7981 } else {
7982 $blog_2_install_map = array();
7983 $site_ids = array();
7984
7985 $all_sites = Freemius::get_sites();
7986
7987 foreach ( $all_sites as $site ) {
7988 $blog_id = Freemius::get_site_blog_id( $site );
7989 $install = $this->get_install_by_blog_id( $blog_id );
7990
7991 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7992 // Skip license activation for installs that are already associated with a license.
7993 continue;
7994 }
7995
7996 if ( is_object( $install ) ) {
7997 $blog_2_install_map[ $blog_id ] = $install;
7998 } else {
7999 $site_ids[] = $blog_id;
8000 }
8001 }
8002
8003 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
8004 return;
8005 }
8006
8007 $user = $this->get_current_or_network_user();
8008
8009 if ( ! empty( $blog_2_install_map ) ) {
8010 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
8011
8012 if ( true !== $result ) {
8013 return;
8014 }
8015 }
8016
8017 if ( ! empty( $site_ids ) ) {
8018 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
8019 }
8020 }
8021 }
8022
8023 /**
8024 * 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.
8025 *
8026 * @author Leo Fajardo (@leorw)
8027 * @since 2.4.0
8028 *
8029 * @param FS_Plugin_License $license
8030 * @param array $sites
8031 * @param int $blog_id
8032 */
8033 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8034 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8035 $license = $this->_license;
8036 }
8037
8038 if ( ! is_object( $license ) ) {
8039 return;
8040 }
8041
8042 $parent_license = ( ! empty( $license->products ) ) ?
8043 $license :
8044 $this->get_active_parent_license( $license->secret_key );
8045
8046 if ( is_object( $parent_license ) ) {
8047 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8048 }
8049 }
8050
8051 /**
8052 * Try to activate a bundle license for all the bundle products installed on the site.
8053 * (1) If a child product install already has a license, the bundle license won't be activated.
8054 * (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.
8055 * (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.
8056 *
8057 * @author Leo Fajardo (@leorw)
8058 * @since 2.4.0
8059 *
8060 * @param FS_Plugin_License $license
8061 * @param array $sites
8062 * @param int $current_blog_id
8063 */
8064 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8065 $is_network_admin = fs_is_network_admin();
8066
8067 $installs_by_blog_map = array();
8068 $site_info_by_blog_map = array();
8069
8070 /**
8071 * Try to activate the license for all supported products.
8072 *
8073 * @author Leo Fajardo
8074 */
8075 foreach ( $license->products as $product_id ) {
8076 $fs = self::get_instance_by_id( $product_id );
8077
8078 if ( ! is_object( $fs ) ) {
8079 continue;
8080 }
8081
8082 if ( ! $fs->has_paid_plan() ) {
8083 continue;
8084 }
8085
8086 if (
8087 ! $fs->is_addon() &&
8088 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8089 ) {
8090 /**
8091 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8092 * there is a context bundle.
8093 */
8094 continue;
8095 }
8096
8097 if ( $current_blog_id > 0 ) {
8098 $fs->switch_to_blog( $current_blog_id );
8099 }
8100
8101 if ( $fs->has_active_valid_license() ) {
8102 continue;
8103 }
8104
8105 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8106 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8107 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8108 continue;
8109 }
8110 } else {
8111 if ( ! $fs->is_network_active() ) {
8112 // Do not try to activate the license in the network level if the product is not network active.
8113 continue;
8114 }
8115
8116 if ( $fs->is_network_delegated_connection() ) {
8117 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8118 continue;
8119 }
8120
8121 $has_install_with_license = false;
8122
8123 // 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.
8124 $filtered_sites = array();
8125
8126 if ( empty( $sites ) ) {
8127 $all_sites = self::get_sites();
8128
8129 foreach ( $all_sites as $site ) {
8130 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8131 }
8132 } else {
8133 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8134 foreach ( $sites as $site ) {
8135 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8136 continue;
8137 }
8138
8139 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8140 }
8141 }
8142
8143 foreach ( $sites as $site ) {
8144 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8145 continue;
8146 }
8147
8148 $blog_id = $site['blog_id'];
8149
8150 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8151 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8152 }
8153
8154 $installs = $installs_by_blog_map[ $blog_id ];
8155 $install = null;
8156
8157 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8158 $install = $installs[ $fs->get_slug() ];
8159
8160 if (
8161 is_object( $install ) &&
8162 (
8163 ! FS_Site::is_valid_id( $install->id ) ||
8164 ! FS_User::is_valid_id( $install->user_id ) ||
8165 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8166 )
8167 ) {
8168 $install = null;
8169 }
8170 }
8171
8172 if (
8173 is_object( $install ) &&
8174 FS_Plugin_License::is_valid_id( $install->license_id )
8175 ) {
8176 $has_install_with_license = true;
8177 break;
8178 }
8179
8180 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8181 // Site activation delegated, don't activate bundle license on the site in the network admin.
8182 continue;
8183 }
8184
8185 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8186 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8187 }
8188
8189 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8190 }
8191
8192 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8193 // 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.
8194 continue;
8195 }
8196
8197 $sites = $filtered_sites;
8198 }
8199
8200 $fs->activate_migrated_license(
8201 $license->secret_key,
8202 null,
8203 null,
8204 $sites,
8205 ( $current_blog_id > 0 ? $current_blog_id : null )
8206 );
8207 }
8208 }
8209
8210 /**
8211 * Returns a parent license that can be activated for the context product.
8212 *
8213 * @author Leo Fajardo (@leorw)
8214 * @since 2.3.0
8215 *
8216 * @param string|null $license_key
8217 * @param bool $flush
8218 *
8219 * @return FS_Plugin_License
8220 */
8221 function get_active_parent_license( $license_key = null, $flush = true ) {
8222 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8223
8224 $fs = $this;
8225
8226 if ( $this->is_addon() ) {
8227 $parent_instance = $this->get_parent_instance();
8228
8229 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8230 $fs = $parent_instance;
8231 }
8232 }
8233
8234 $foreign_licenses = $fs->get_foreign_licenses_info(
8235 self::get_all_licenses( $this->get_parent_id() )
8236 );
8237
8238 if ( ! empty ( $foreign_licenses ) ) {
8239 $foreign_licenses = array(
8240 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8241 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8242 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8243 );
8244
8245 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8246 }
8247
8248 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8249
8250 if (
8251 ! $this->is_api_result_object( $result, 'licenses' ) ||
8252 ! is_array( $result->licenses ) ||
8253 empty( $result->licenses )
8254 ) {
8255 return null;
8256 }
8257
8258 $parent_license = null;
8259
8260 if ( empty( $license_key ) ) {
8261 $parent_license = $result->licenses[0];
8262 } else {
8263 foreach ( $result->licenses as $license ) {
8264 if ( $license_key === $license->secret_key ) {
8265 $parent_license = $license;
8266 break;
8267 }
8268 }
8269 }
8270
8271 if ( ! is_null( $parent_license ) ) {
8272 $parent_license = new FS_Plugin_License( $parent_license );
8273 }
8274
8275 return $parent_license;
8276 }
8277
8278 /**
8279 * @author Leo Fajardo (@leorw)
8280 * @since 2.3.0
8281 *
8282 * @return array
8283 */
8284 function get_sites_for_network_level_optin() {
8285 $sites = array();
8286 $all_sites = self::get_sites();
8287
8288 foreach ( $all_sites as $site ) {
8289 $blog_id = self::get_site_blog_id( $site );
8290
8291 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8292 ! $this->is_installed_on_site( $blog_id )
8293 ) {
8294 $sites[] = $this->get_site_info( $site );
8295 }
8296 }
8297
8298 return $sites;
8299 }
8300
8301 /**
8302 * Delete account.
8303 *
8304 * @author Vova Feldman (@svovaf)
8305 * @since 1.0.3
8306 *
8307 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8308 */
8309 function delete_account_event( $check_user = true ) {
8310 $this->_logger->entrance( 'slug = ' . $this->_slug );
8311
8312 if ( $check_user && ! $this->is_user_admin() ) {
8313 return;
8314 }
8315
8316 $this->do_action( 'before_account_delete' );
8317
8318 // Clear all admin notices.
8319 $this->_admin_notices->clear_all_sticky( false );
8320
8321 $this->_delete_site( false );
8322
8323 $delete_network_common_data = true;
8324
8325 if ( $this->_is_network_active ) {
8326 $installs = $this->get_blog_install_map();
8327
8328 // Don't delete common network data unless no other installs left.
8329 $delete_network_common_data = empty( $installs );
8330 }
8331
8332 if ( $delete_network_common_data ) {
8333 $this->_delete_plans( false );
8334
8335 $this->_delete_licenses( false );
8336
8337 // Delete add-ons related to plugin's account.
8338 $this->_delete_account_addons( false );
8339 }
8340
8341 // @todo Delete plans and licenses of add-ons.
8342
8343 self::$_accounts->store();
8344
8345 /**
8346 * IMPORTANT:
8347 * Clear crons must be executed before clearing all storage.
8348 * Otherwise, the cron will not be cleared.
8349 */
8350 if ( $delete_network_common_data ) {
8351 $this->clear_sync_cron();
8352 }
8353
8354 $this->clear_install_sync_cron();
8355
8356 // Clear all storage data.
8357 $this->_storage->clear_all( true, array(
8358 'is_delegated_connection',
8359 'connectivity_test',
8360 'is_on',
8361 ), false );
8362
8363 // Send delete event.
8364 $this->get_api_site_scope()->call( '/', 'delete' );
8365
8366 $this->do_action( 'after_account_delete' );
8367 }
8368
8369 /**
8370 * Delete network level account.
8371 *
8372 * @author Vova Feldman (@svovaf)
8373 * @since 2.0.0
8374 *
8375 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8376 */
8377 function delete_network_account_event( $check_user = true ) {
8378 $this->_logger->entrance( 'slug = ' . $this->_slug );
8379
8380 if ( $check_user && ! $this->is_user_admin() ) {
8381 return;
8382 }
8383
8384 $this->do_action( 'before_network_account_delete' );
8385
8386 // Clear all admin notices.
8387 $this->_admin_notices->clear_all_sticky();
8388
8389 $this->_delete_plans( false, false );
8390
8391 $this->_delete_licenses( false );
8392
8393 // Delete add-ons related to plugin's account.
8394 $this->_delete_account_addons( false );
8395
8396 // @todo Delete plans and licenses of add-ons.
8397
8398 self::$_accounts->store( true );
8399
8400 /**
8401 * IMPORTANT:
8402 * Clear crons must be executed before clearing all storage.
8403 * Otherwise, the cron will not be cleared.
8404 */
8405 $this->clear_sync_cron( true );
8406 $this->clear_install_sync_cron( true );
8407
8408 $sites = self::get_sites();
8409
8410 $install_ids = array();
8411 foreach ( $sites as $site ) {
8412 $blog_id = self::get_site_blog_id( $site );
8413
8414 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8415 continue;
8416 }
8417
8418 $install_id = $this->_delete_site( true, $blog_id );
8419
8420 // Clear all storage data.
8421 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8422
8423 if ( FS_Site::is_valid_id( $install_id ) ) {
8424 $install_ids[] = $install_id;
8425 }
8426
8427 switch_to_blog( $blog_id );
8428
8429 $this->do_action( 'after_account_delete' );
8430
8431 restore_current_blog();
8432 }
8433
8434 $this->_storage->clear_all( true, array(
8435 'connectivity_test',
8436 'is_on',
8437 ), true );
8438
8439 // Send delete event.
8440 if ( ! empty( $install_ids ) ) {
8441 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8442 }
8443
8444 $this->do_action( 'after_network_account_delete' );
8445 }
8446
8447 /**
8448 * Plugin deactivation hook.
8449 *
8450 * @author Vova Feldman (@svovaf)
8451 * @since 1.0.1
8452 */
8453 function _deactivate_plugin_hook() {
8454 $this->_logger->entrance( 'slug = ' . $this->_slug );
8455
8456 if ( ! $this->is_user_admin() ) {
8457 return;
8458 }
8459
8460 $is_network_deactivation = fs_is_network_admin();
8461 $storage_keys_for_removal = array();
8462
8463 $this->_admin_notices->clear_all_sticky();
8464
8465 $storage_keys_for_removal[] = 'sticky_optin_added';
8466 if ( isset( $this->_storage->sticky_optin_added ) ) {
8467 unset( $this->_storage->sticky_optin_added );
8468 }
8469
8470 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8471 // Remember that plugin was already installed.
8472 $this->_storage->is_plugin_new_install = false;
8473 }
8474
8475 // Hook to plugin uninstall.
8476 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8477
8478 $this->clear_module_main_file_cache();
8479 $this->clear_sync_cron( $this->_is_network_active );
8480 $this->clear_install_sync_cron();
8481
8482 if ( $this->is_registered() ) {
8483 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8484 FS_Plugin_Updater::instance( $this )->delete_update_data();
8485 }
8486
8487 if ( $is_network_deactivation ) {
8488 // Send deactivation event.
8489 $this->sync_installs( array(
8490 'is_active' => false,
8491 ) );
8492 } else {
8493 // Send deactivation event.
8494 $this->sync_install( array(
8495 'is_active' => false,
8496 ) );
8497 }
8498 } else {
8499 if ( ! $this->has_api_connectivity() ) {
8500 // Reset connectivity test cache.
8501 unset( $this->_storage->connectivity_test );
8502
8503 $storage_keys_for_removal[] = 'connectivity_test';
8504 }
8505 }
8506
8507 if ( $is_network_deactivation ) {
8508 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8509 unset( $this->_storage->sticky_optin_added_ms );
8510 }
8511
8512 if ( ! empty( $storage_keys_for_removal ) ) {
8513 $sites = self::get_sites();
8514
8515 foreach ( $sites as $site ) {
8516 $blog_id = self::get_site_blog_id( $site );
8517
8518 foreach ( $storage_keys_for_removal as $key ) {
8519 $this->_storage->remove( $key, false, $blog_id );
8520 }
8521
8522 $this->_storage->save( $blog_id );
8523 }
8524 }
8525 }
8526
8527 // Clear API cache on deactivation.
8528 FS_Api::clear_cache();
8529
8530 $this->remove_sdk_reference();
8531 }
8532
8533 /**
8534 * @author Vova Feldman (@svovaf)
8535 * @since 1.1.6
8536 */
8537 private function remove_sdk_reference() {
8538 global $fs_active_plugins;
8539
8540 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8541 if ( $this->_plugin_basename == $data->plugin_path ) {
8542 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8543 break;
8544 }
8545 }
8546
8547 fs_fallback_to_newest_active_sdk();
8548 }
8549
8550 /**
8551 * @author Vova Feldman (@svovaf)
8552 * @since 1.1.3
8553 *
8554 * @param bool $is_anonymous
8555 * @param bool|int $network_or_blog_id Since 2.0.0
8556 */
8557 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8558 // Store information regarding skip to try and opt-in the user
8559 // again in the future.
8560 $skip_info = array(
8561 'is' => $is_anonymous,
8562 'timestamp' => WP_FS__SCRIPT_START_TIME,
8563 'version' => $this->get_plugin_version(),
8564 );
8565
8566 if ( true === $network_or_blog_id ) {
8567 $this->_storage->is_anonymous_ms = $skip_info;
8568 } else {
8569 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8570 }
8571
8572 $this->network_upgrade_mode_completed();
8573
8574 // Update anonymous mode cache.
8575 $this->_is_anonymous = $is_anonymous;
8576 }
8577
8578 /**
8579 * @author Vova Feldman (@svovaf)
8580 * @since 2.0.0
8581 *
8582 * @param int $blog_id Site ID.
8583 * @param int $user_id User ID.
8584 * @param string $domain Site domain.
8585 * @param string $path Site path.
8586 * @param int $network_id Network ID. Only relevant on multi-network installations.
8587 * @param array $meta Metadata. Used to set initial site options.
8588 *
8589 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8590 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8591 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8592 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8593 */
8594 function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8595 $this->_logger->entrance();
8596
8597 if ( $this->is_premium() &&
8598 $this->is_network_connected() &&
8599 is_object( $this->_license ) &&
8600 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8601 $this->is_license_network_active( $blog_id )
8602 ) {
8603 /**
8604 * 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.
8605 */
8606 $current_blog_id = get_current_blog_id();
8607 $license = clone $this->_license;
8608
8609 $this->switch_to_blog( $blog_id );
8610
8611 // Opt-in with network user.
8612 $this->install_with_user(
8613 $this->get_network_user(),
8614 $license->secret_key,
8615 false,
8616 false,
8617 false
8618 );
8619
8620 if ( is_object( $this->_site ) ) {
8621 if ( $this->_site->license_id == $license->id ) {
8622 /**
8623 * If the license was activated successfully, sync the license data from the remote server.
8624 */
8625 $this->_license = $license;
8626 $this->sync_site_license();
8627 }
8628 }
8629
8630 $this->switch_to_blog( $current_blog_id );
8631
8632 if ( is_object( $this->_site ) ) {
8633 // Already connected (with or without a license), so no need to continue.
8634 return;
8635 }
8636 }
8637
8638 if ( $this->is_network_anonymous() ) {
8639 /**
8640 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8641 */
8642 $this->skip_site_connection( $blog_id );
8643 } else if ( $this->is_network_delegated_connection() ) {
8644 /**
8645 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8646 */
8647 $this->delegate_site_connection( $blog_id );
8648 } else if ( $this->is_network_connected() ) {
8649 /**
8650 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8651 */
8652 $current_blog_id = get_current_blog_id();
8653
8654 $this->switch_to_blog( $blog_id );
8655
8656 // Opt-in with network user.
8657 $this->install_with_user(
8658 $this->get_network_user(),
8659 false,
8660 false,
8661 false,
8662 false
8663 );
8664
8665 $this->switch_to_blog( $current_blog_id );
8666 } else {
8667 /**
8668 * If the super-admin mixed different options (connect, skip, delegated):
8669 * a) If at least one site connection was delegated, then automatically delegate connection.
8670 * 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.
8671 */
8672 $has_delegated_site = false;
8673
8674 $sites = self::get_sites();
8675 foreach ( $sites as $site ) {
8676 $blog_id = self::get_site_blog_id( $site );
8677
8678 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8679 $has_delegated_site = true;
8680 break;
8681 }
8682 }
8683
8684 if ( $has_delegated_site ) {
8685 $this->delegate_site_connection( $blog_id );
8686 } else {
8687 $this->skip_site_connection( $blog_id );
8688 }
8689 }
8690 }
8691
8692 /**
8693 * @author Vova Feldman (@svovaf)
8694 * @since 1.1.3
8695 *
8696 * @param bool|int $network_or_blog_id Since 2.0.0.
8697 */
8698 private function reset_anonymous_mode( $network_or_blog_id = 0 ) {
8699 if ( true === $network_or_blog_id ) {
8700 unset( $this->_storage->is_anonymous_ms );
8701 } else {
8702 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8703 }
8704
8705 /**
8706 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8707 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8708 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8709 *
8710 * @author Leo Fajardo (@leorw)
8711 * @since 1.2.2
8712 */
8713 if ( ! $this->_is_network_active ||
8714 0 === $network_or_blog_id ||
8715 get_current_blog_id() == $network_or_blog_id ||
8716 ( true === $network_or_blog_id && fs_is_network_admin() )
8717 ) {
8718 $this->_is_anonymous = null;
8719 }
8720 }
8721
8722 /**
8723 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8724 * deleting the account in the network level, the URL of the page to redirect to is correct.
8725 *
8726 * @author Leo Fajardo (@leorw)
8727 *
8728 * @since 2.1.3
8729 */
8730 private function maybe_set_slug_and_network_menu_exists_flag() {
8731 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8732 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8733 $this->_menu->get_slug() :
8734 $this->_slug
8735 );
8736 }
8737 }
8738
8739 /**
8740 * Clears the anonymous mode and redirects to the opt-in screen.
8741 *
8742 * @author Vova Feldman (@svovaf)
8743 * @since 1.1.7
8744 */
8745 function connect_again() {
8746 if ( ! $this->is_anonymous() ) {
8747 return;
8748 }
8749
8750 $this->reset_anonymous_mode( fs_is_network_admin() );
8751
8752 $this->maybe_set_slug_and_network_menu_exists_flag();
8753
8754 fs_redirect( $this->get_activation_url() );
8755 }
8756
8757 /**
8758 * Skip account connect, and set anonymous mode.
8759 *
8760 * @author Vova Feldman (@svovaf)
8761 * @since 1.1.1
8762 *
8763 * @param array|null $sites Since 2.0.0. Specific sites.
8764 * @param bool $skip_all_network Since 2.0.0. If true, skip connection for all sites.
8765 */
8766 function skip_connection( $sites = null, $skip_all_network = false ) {
8767 $this->_logger->entrance();
8768
8769 $this->_admin_notices->remove_sticky( 'connect_account' );
8770
8771 if ( $skip_all_network ) {
8772 $this->set_anonymous_mode( true, true );
8773 }
8774
8775 if ( ! $skip_all_network && empty( $sites ) ) {
8776 $this->skip_site_connection();
8777 } else {
8778 $uids = array();
8779
8780 if ( $skip_all_network ) {
8781 $this->set_anonymous_mode( true, true );
8782
8783 $sites = self::get_sites();
8784 foreach ( $sites as $site ) {
8785 $blog_id = self::get_site_blog_id( $site );
8786 $this->skip_site_connection( $blog_id, false );
8787 $uids[] = $this->get_anonymous_id( $blog_id );
8788 }
8789 } else if ( ! empty( $sites ) ) {
8790 foreach ( $sites as $site ) {
8791 $uids[] = $site['uid'];
8792 $this->skip_site_connection( $site['blog_id'], false );
8793 }
8794 }
8795
8796 // Send anonymous skip event.
8797 // No user identified info nor any tracking will be sent after the user skips the opt-in.
8798 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8799 'uids' => $uids,
8800 ) );
8801 }
8802
8803 $this->network_upgrade_mode_completed();
8804 }
8805
8806 /**
8807 * Skip connection for specific site in the network.
8808 *
8809 * @author Vova Feldman (@svovaf)
8810 * @since 2.0.0
8811 *
8812 * @param int|null $blog_id
8813 * @param bool $send_skip
8814 */
8815 private function skip_site_connection( $blog_id = null, $send_skip = true ) {
8816 $this->_logger->entrance();
8817
8818 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8819
8820 $this->set_anonymous_mode( true, $blog_id );
8821
8822 if ( $send_skip ) {
8823 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8824 'uids' => array( $this->get_anonymous_id( $blog_id ) ),
8825 ) );
8826 }
8827 }
8828
8829 /**
8830 * Plugin version update hook.
8831 *
8832 * @author Vova Feldman (@svovaf)
8833 * @since 1.0.4
8834 */
8835 private function update_plugin_version_event() {
8836 $this->_logger->entrance();
8837
8838 if ( ! $this->is_registered() ) {
8839 return;
8840 }
8841
8842 $this->schedule_install_sync();
8843 // $this->sync_install( array(), true );
8844 }
8845
8846 /**
8847 * Generate an MD5 signature of a plugins collection.
8848 * This helper methods used to identify changes in a plugins collection.
8849 *
8850 * @author Vova Feldman (@svovaf)
8851 * @since 2.0.0
8852 *
8853 * @param array [string]array $plugins
8854 *
8855 * @return string
8856 */
8857 private function get_plugins_thumbprint( $plugins ) {
8858 ksort( $plugins );
8859
8860 $thumbprint = '';
8861 foreach ( $plugins as $basename => $data ) {
8862 $thumbprint .= $data['slug'] . ',' .
8863 $data['Version'] . ',' .
8864 ( $data['is_active'] ? '1' : '0' ) . ';';
8865 }
8866
8867 return md5( $thumbprint );
8868 }
8869
8870 /**
8871 * Return a list of modified plugins since the last sync.
8872 *
8873 * Note:
8874 * There's no point to store a plugins counter since even if the number of
8875 * plugins didn't change, we still need to check if the versions are all the
8876 * same and the activity state is similar.
8877 *
8878 * @author Vova Feldman (@svovaf)
8879 * @since 1.1.8
8880 *
8881 * @return array|false
8882 */
8883 private function get_plugins_data_for_api() {
8884 // Alias.
8885 $site_active_plugins_option_name = 'active_plugins';
8886 $network_plugins_option_name = 'all_plugins';
8887
8888 /**
8889 * Collection of all site level active plugins.
8890 */
8891 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8892
8893 if ( ! is_object( $site_active_plugins_cache ) ) {
8894 $site_active_plugins_cache = (object) array(
8895 'timestamp' => '',
8896 'md5' => '',
8897 'plugins' => array(),
8898 );
8899 }
8900
8901 $time = time();
8902
8903 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8904 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8905 ) {
8906 // Don't send plugin updates if last update was in the past 5 min.
8907 return false;
8908 }
8909
8910 // Write timestamp to lock the logic.
8911 $site_active_plugins_cache->timestamp = $time;
8912 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8913
8914 // Reload options from DB.
8915 self::$_accounts->load( true );
8916 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8917
8918 if ( $time != $site_active_plugins_cache->timestamp ) {
8919 // If timestamp is different, then another thread captured the lock.
8920 return false;
8921 }
8922
8923 /**
8924 * Collection of all plugins (network level).
8925 */
8926 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8927
8928 if ( ! is_object( $network_plugins_cache ) ) {
8929 $network_plugins_cache = (object) array(
8930 'timestamp' => '',
8931 'md5' => '',
8932 'plugins' => array(),
8933 );
8934 }
8935
8936 // Check if there's a change in plugins.
8937 $network_plugins = self::get_network_plugins();
8938 $site_active_plugins = self::get_site_active_plugins();
8939
8940 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8941 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8942
8943 // Check if plugins status changed (version or active/inactive).
8944 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8945 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8946
8947 if ( ! $network_plugins_changed &&
8948 ! $site_active_plugins_changed
8949 ) {
8950 // No changes.
8951 return array();
8952 }
8953
8954 $plugins_update_data = array();
8955
8956 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8957 if ( ! isset( $network_plugins[ $basename ] ) ) {
8958 // Plugin uninstalled.
8959 $uninstalled_plugin_data = $data;
8960 $uninstalled_plugin_data['is_active'] = false;
8961 $uninstalled_plugin_data['is_uninstalled'] = true;
8962 $plugins_update_data[] = $uninstalled_plugin_data;
8963
8964 unset( $network_plugins[ $basename ] );
8965
8966 unset( $network_plugins_cache->plugins[ $basename ] );
8967 unset( $site_active_plugins_cache->plugins[ $basename ] );
8968
8969 continue;
8970 }
8971
8972 $was_active = $data['is_active'] ||
8973 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8974 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8975 $is_active = $network_plugins[ $basename ]['is_active'] ||
8976 ( isset( $site_active_plugins[ $basename ] ) &&
8977 $site_active_plugins[ $basename ]['is_active'] );
8978
8979 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8980 isset( $site_active_plugins[ $basename ] )
8981 ) {
8982 // Plugin was site level activated.
8983 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
8984 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8985 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8986 ! isset( $site_active_plugins[ $basename ] )
8987 ) {
8988 // Plugin was site level deactivated.
8989 unset( $site_active_plugins_cache->plugins[ $basename ] );
8990 }
8991
8992 $prev_version = $data['version'];
8993 $current_version = $network_plugins[ $basename ]['Version'];
8994
8995 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8996 // Plugin activated or deactivated, or version changed.
8997
8998 if ( $was_active !== $is_active ) {
8999 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9000 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9001 }
9002 }
9003
9004 if ( $prev_version !== $current_version ) {
9005 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9006 }
9007
9008 $updated_plugin_data = $data;
9009 $updated_plugin_data['is_active'] = $is_active;
9010 $updated_plugin_data['version'] = $current_version;
9011 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9012 $plugins_update_data[] = $updated_plugin_data;
9013 }
9014 }
9015
9016 // Find new plugins that weren't yet seen before.
9017 foreach ( $network_plugins as $basename => $data ) {
9018 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9019 // New plugin.
9020 $new_plugin = array(
9021 'slug' => $data['slug'],
9022 'version' => $data['Version'],
9023 'title' => $data['Name'],
9024 'is_active' => $data['is_active'],
9025 'is_uninstalled' => false,
9026 );
9027
9028 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9029
9030 $is_site_level_active = (
9031 isset( $site_active_plugins[ $basename ] ) &&
9032 $site_active_plugins[ $basename ]['is_active']
9033 );
9034
9035 /**
9036 * If not network active, set the activity status based on the site-level plugin status.
9037 */
9038 if ( ! $new_plugin['is_active'] ) {
9039 $new_plugin['is_active'] = $is_site_level_active;
9040 }
9041
9042 $plugins_update_data[] = $new_plugin;
9043
9044 if ( isset( $site_active_plugins[ $basename ] ) ) {
9045 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9046 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9047 }
9048 }
9049 }
9050
9051 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9052 $site_active_plugins_cache->timestamp = $time;
9053 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9054
9055 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9056 $network_plugins_cache->timestamp = $time;
9057 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9058
9059 return $plugins_update_data;
9060 }
9061
9062 /**
9063 * Return a list of modified themes since the last sync.
9064 *
9065 * Note:
9066 * There's no point to store a themes counter since even if the number of
9067 * themes didn't change, we still need to check if the versions are all the
9068 * same and the activity state is similar.
9069 *
9070 * @author Vova Feldman (@svovaf)
9071 * @since 1.1.8
9072 *
9073 * @return array|false
9074 */
9075 private function get_themes_data_for_api() {
9076 // Alias.
9077 $option_name = 'all_themes';
9078
9079 $all_cached_themes = self::$_accounts->get_option( $option_name );
9080
9081 if ( ! is_object( $all_cached_themes ) ) {
9082 $all_cached_themes = (object) array(
9083 'timestamp' => '',
9084 'md5' => '',
9085 'themes' => array(),
9086 );
9087 }
9088
9089 $time = time();
9090
9091 if ( ! empty( $all_cached_themes->timestamp ) &&
9092 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9093 ) {
9094 // Don't send theme updates if last update was in the past 5 min.
9095 return false;
9096 }
9097
9098 // Write timestamp to lock the logic.
9099 $all_cached_themes->timestamp = $time;
9100 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9101
9102 // Reload options from DB.
9103 self::$_accounts->load( true );
9104 $all_cached_themes = self::$_accounts->get_option( $option_name );
9105
9106 if ( $time != $all_cached_themes->timestamp ) {
9107 // If timestamp is different, then another thread captured the lock.
9108 return false;
9109 }
9110
9111 // Get active theme.
9112 $active_theme = wp_get_theme();
9113 $active_theme_stylesheet = $active_theme->get_stylesheet();
9114
9115 // Check if there's a change in themes.
9116 $all_themes = wp_get_themes();
9117
9118 // Check if themes changed.
9119 ksort( $all_themes );
9120
9121 $themes_signature = '';
9122 foreach ( $all_themes as $slug => $data ) {
9123 $is_active = ( $slug === $active_theme_stylesheet );
9124 $themes_signature .= $slug . ',' .
9125 $data->version . ',' .
9126 ( $is_active ? '1' : '0' ) . ';';
9127 }
9128
9129 // Check if themes status changed (version or active/inactive).
9130 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9131
9132 $themes_update_data = array();
9133
9134 if ( $themes_changed ) {
9135 // Change in themes, report changes.
9136
9137 // Update existing themes info.
9138 foreach ( $all_cached_themes->themes as $slug => $data ) {
9139 $is_active = ( $slug === $active_theme_stylesheet );
9140
9141 if ( ! isset( $all_themes[ $slug ] ) ) {
9142 // Plugin uninstalled.
9143 $uninstalled_theme_data = $data;
9144 $uninstalled_theme_data['is_active'] = false;
9145 $uninstalled_theme_data['is_uninstalled'] = true;
9146 $themes_update_data[] = $uninstalled_theme_data;
9147
9148 unset( $all_themes[ $slug ] );
9149 unset( $all_cached_themes->themes[ $slug ] );
9150 } else if ( $data['is_active'] !== $is_active ||
9151 $data['version'] !== $all_themes[ $slug ]->version
9152 ) {
9153 // Plugin activated or deactivated, or version changed.
9154
9155 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9156 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9157
9158 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9159 }
9160 }
9161
9162 // Find new themes that weren't yet seen before.
9163 foreach ( $all_themes as $slug => $data ) {
9164 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9165 $is_active = ( $slug === $active_theme_stylesheet );
9166
9167 // New plugin.
9168 $new_plugin = array(
9169 'slug' => $slug,
9170 'version' => $data->version,
9171 'title' => $data->name,
9172 'is_active' => $is_active,
9173 'is_uninstalled' => false,
9174 );
9175
9176 $themes_update_data[] = $new_plugin;
9177 $all_cached_themes->themes[ $slug ] = $new_plugin;
9178 }
9179 }
9180
9181 $all_cached_themes->md5 = md5( $themes_signature );
9182 $all_cached_themes->timestamp = time();
9183 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9184 }
9185
9186 return $themes_update_data;
9187 }
9188
9189 /**
9190 * Get site data for API install request.
9191 *
9192 * @author Vova Feldman (@svovaf)
9193 * @since 1.1.2
9194 *
9195 * @param string[] $override
9196 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9197 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9198 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, charset, title, and URL).
9199 *
9200 * @return array
9201 */
9202 private function get_install_data_for_api(
9203 array $override,
9204 $include_plugins = true,
9205 $include_themes = true,
9206 $include_blog_data = true
9207 ) {
9208 if ( $this->is_extensions_tracking_allowed() ) {
9209 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9210 /**
9211 * @since 1.1.8 Also send plugin updates.
9212 */
9213 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9214 $plugins = $this->get_plugins_data_for_api();
9215 if ( ! empty( $plugins ) ) {
9216 $override['plugins'] = $plugins;
9217 }
9218 }
9219 }
9220
9221 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9222 /**
9223 * @since 1.1.8 Also send themes updates.
9224 */
9225 if ( $include_themes && ! isset( $override['themes'] ) ) {
9226 $themes = $this->get_themes_data_for_api();
9227 if ( ! empty( $themes ) ) {
9228 $override['themes'] = $themes;
9229 }
9230 }
9231 }
9232 }
9233
9234 $versions = $this->get_versions();
9235
9236 $blog_data = $include_blog_data ?
9237 array(
9238 'language' => get_bloginfo( 'language' ),
9239 'charset' => get_bloginfo( 'charset' ),
9240 'title' => get_bloginfo( 'name' ),
9241 'url' => get_site_url(),
9242 ) :
9243 array();
9244
9245 return array_merge( $versions, $blog_data, array(
9246 'version' => $this->get_plugin_version(),
9247 'is_premium' => $this->is_premium(),
9248 // Special params.
9249 'is_active' => true,
9250 'is_disconnected' => $this->is_tracking_prohibited(),
9251 'is_uninstalled' => false,
9252 ), $override );
9253 }
9254
9255 /**
9256 * Update installs details.
9257 *
9258 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9259 *
9260 * @author Vova Feldman (@svovaf)
9261 * @since 2.0.0
9262 *
9263 * @param string[] string $override
9264 * @param bool $only_diff
9265 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9266 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9267 *
9268 * @return array
9269 */
9270 private function get_installs_data_for_api(
9271 array $override,
9272 $only_diff = false,
9273 $include_plugins = true,
9274 $include_themes = true
9275 ) {
9276 /**
9277 * @since 1.1.8 Also send plugin updates.
9278 */
9279 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9280 // $plugins = $this->get_plugins_data_for_api();
9281 // if ( ! empty( $plugins ) ) {
9282 // $override['plugins'] = $plugins;
9283 // }
9284 // }
9285 /**
9286 * @since 1.1.8 Also send themes updates.
9287 */
9288 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9289 // $themes = $this->get_themes_data_for_api();
9290 // if ( ! empty( $themes ) ) {
9291 // $override['themes'] = $themes;
9292 // }
9293 // }
9294
9295 // Common properties.
9296 $versions = $this->get_versions();
9297 $common = array_merge( $versions, array(
9298 'version' => $this->get_plugin_version(),
9299 'is_premium' => $this->is_premium(),
9300 ), $override );
9301
9302
9303 $is_common_diff_for_any_site = false;
9304 $common_diff_union = array();
9305
9306 $installs_data = array();
9307
9308 $sites = self::get_sites();
9309
9310 foreach ( $sites as $site ) {
9311 $blog_id = self::get_site_blog_id( $site );
9312
9313 $install = $this->get_install_by_blog_id( $blog_id );
9314
9315 if ( is_object( $install ) ) {
9316 if ( $install->user_id != $this->_user->id ) {
9317 // Install belongs to a different owner.
9318 continue;
9319 }
9320
9321 if ( ! $this->is_premium() && $install->is_tracking_prohibited() ) {
9322 // Don't send updates regarding opted-out installs.
9323 continue;
9324 }
9325
9326 $install_data = $this->get_site_info( $site );
9327
9328 $uid = $install_data['uid'];
9329
9330 unset( $install_data['blog_id'] );
9331 unset( $install_data['uid'] );
9332
9333 $install_data['is_disconnected'] = $install->is_disconnected;
9334 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9335 $install_data['is_uninstalled'] = $install->is_uninstalled;
9336
9337 $common_diff = null;
9338 $is_common_diff = false;
9339 if ( $only_diff ) {
9340 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9341 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9342
9343 $is_common_diff = ! empty( $common_diff );
9344
9345 if ( $is_common_diff ) {
9346 foreach ( $common_diff as $k => $v ) {
9347 if ( ! isset( $common_diff_union[ $k ] ) ) {
9348 $common_diff_union[ $k ] = $v;
9349 }
9350 }
9351 }
9352
9353 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9354 }
9355
9356 if ( ! empty( $install_data ) || $is_common_diff ) {
9357 // Add install ID and site unique ID.
9358 $install_data['id'] = $install->id;
9359 $install_data['uid'] = $uid;
9360
9361 $installs_data[] = $install_data;
9362 }
9363 }
9364 }
9365
9366 restore_current_blog();
9367
9368 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9369 if ( ! $only_diff ) {
9370 $installs_data[] = $common;
9371 } else if ( ! empty( $common_diff_union ) ) {
9372 $installs_data[] = $common_diff_union;
9373 }
9374 }
9375
9376 foreach ( $installs_data as &$data ) {
9377 $data = (object) $data;
9378 }
9379
9380 return $installs_data;
9381 }
9382
9383 /**
9384 * Compare site actual data to the stored install data and return the differences for an API data sync.
9385 *
9386 * @author Vova Feldman (@svovaf)
9387 * @since 2.0.0
9388 *
9389 * @param array $site
9390 * @param FS_Site $install
9391 * @param string[] string $override
9392 *
9393 * @return array
9394 */
9395 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9396 $diff = array();
9397 $special = array();
9398 $special_override = false;
9399
9400 foreach ( $site as $p => $v ) {
9401 if ( property_exists( $install, $p ) ) {
9402 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9403 $install->{$p} != $v
9404 ) {
9405 $install->{$p} = $v;
9406 $diff[ $p ] = $v;
9407 }
9408 } else {
9409 $special[ $p ] = $v;
9410
9411 if ( isset( $override[ $p ] ) ||
9412 'plugins' === $p ||
9413 'themes' === $p
9414 ) {
9415 $special_override = true;
9416 }
9417 }
9418 }
9419
9420 if ( $special_override || 0 < count( $diff ) ) {
9421 // Add special params only if has at least one
9422 // standard param, or if explicitly requested to
9423 // override a special param or a param which is not exist
9424 // in the install object.
9425 $diff = array_merge( $diff, $special );
9426 }
9427
9428 return $diff;
9429 }
9430
9431 /**
9432 * Update install only if changed.
9433 *
9434 * @author Vova Feldman (@svovaf)
9435 * @since 1.0.9
9436 *
9437 * @param string[] string $override
9438 * @param bool $flush
9439 *
9440 * @return false|object|string
9441 */
9442 private function send_install_update( $override = array(), $flush = false ) {
9443 $this->_logger->entrance();
9444
9445 $check_properties = $this->get_install_data_for_api( $override );
9446
9447 if ( $flush ) {
9448 $params = $check_properties;
9449 } else {
9450 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9451 }
9452
9453 $keepalive_only_update = false;
9454 if ( empty( $params ) ) {
9455 $keepalive_only_update = $this->should_send_keepalive_update();
9456
9457 if ( ! $keepalive_only_update ) {
9458 /**
9459 * There are no updates to send including keepalive.
9460 *
9461 * @author Leo Fajardo (@leorw)
9462 * @since 2.2.3
9463 */
9464 return false;
9465 }
9466 }
9467
9468 if ( ! $keepalive_only_update ) {
9469 /**
9470 * Do not update the last install sync timestamp after a keepalive-only call since there were no actual
9471 * updates sent.
9472 *
9473 * @author Leo Fajardo (@leorw)
9474 * @since 2.2.3
9475 */
9476 if ( ! is_multisite() ) {
9477 // Update last install sync timestamp.
9478 $this->set_cron_execution_timestamp( 'install_sync' );
9479 }
9480
9481 $params['uid'] = $this->get_anonymous_id();
9482 }
9483
9484 $this->set_keepalive_timestamp();
9485
9486 // Send updated values to FS.
9487 $site = $this->get_api_site_scope()->call( '/', 'put', $params );
9488
9489 if ( ! $keepalive_only_update && $this->is_api_result_entity( $site ) ) {
9490 /**
9491 * Do not clear scheduled sync after a keepalive-only call since there were no actual updates sent.
9492 *
9493 * @author Leo Fajardo (@leorw)
9494 * @since 2.2.3
9495 */
9496 if ( ! is_multisite() ) {
9497 // I successfully sent install update, clear scheduled sync if exist.
9498 $this->clear_install_sync_cron();
9499 }
9500 }
9501
9502 return $site;
9503 }
9504
9505 /**
9506 * Update installs only if changed.
9507 *
9508 * @author Vova Feldman (@svovaf)
9509 * @since 2.0.0
9510 *
9511 * @param string[] string $override
9512 * @param bool $flush
9513 *
9514 * @return false|object|string
9515 */
9516 private function send_installs_update( $override = array(), $flush = false ) {
9517 $this->_logger->entrance();
9518
9519 $installs_data = $this->get_installs_data_for_api( $override, ! $flush );
9520
9521 $keepalive_only_update = false;
9522 if ( empty( $installs_data ) ) {
9523 /**
9524 * Pass `true` to use the network level storage since the update is for many installs.
9525 *
9526 * @author Leo Fajardo (@leorw)
9527 * @since 2.2.3
9528 */
9529 $keepalive_only_update = $this->should_send_keepalive_update( true );
9530
9531 if ( ! $keepalive_only_update ) {
9532 /**
9533 * There are no updates to send including keepalive.
9534 *
9535 * @author Leo Fajardo (@leorw)
9536 * @since 2.2.3
9537 */
9538 return false;
9539 }
9540 }
9541
9542 if ( ! $keepalive_only_update ) {
9543 // Update last install sync timestamp if there were actual updates sent (i.e., not a keepalive-only call).
9544 $this->set_cron_execution_timestamp( 'install_sync' );
9545 }
9546
9547 /**
9548 * Pass `true` to use the network level storage since the update is for many installs.
9549 *
9550 * @author Leo Fajardo (@leorw)
9551 * @since 2.2.3
9552 */
9553 $this->set_keepalive_timestamp( true );
9554
9555 // Send updated values to FS.
9556 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9557
9558 if ( ! $keepalive_only_update && $this->is_api_result_object( $result, 'installs' ) ) {
9559 // 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.
9560 $this->clear_install_sync_cron();
9561 }
9562
9563 return $result;
9564 }
9565
9566 /**
9567 * @author Leo Fajardo (@leorw)
9568 *
9569 * @param bool|null $use_network_level_storage
9570 *
9571 * @return bool
9572 */
9573 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9574 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9575
9576 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9577 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9578 return true;
9579 } else {
9580 // 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.
9581 return ( 7 == rand( 1, 7 ) );
9582 }
9583 }
9584
9585 /**
9586 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9587 *
9588 * @author Leo Fajardo (@leorw)
9589 * @since 2.3.2
9590 */
9591 private function maybe_sync_install_user() {
9592 if ( $this->_user->id == $this->_site->user_id ) {
9593 return;
9594 }
9595
9596 // Fetch user data and store if found.
9597 $this->sync_user_by_current_install();
9598 }
9599
9600 /**
9601 * Update install only if changed.
9602 *
9603 * @author Vova Feldman (@svovaf)
9604 * @since 1.0.9
9605 *
9606 * @param string[] string $override
9607 * @param bool $flush
9608 */
9609 private function sync_install( $override = array(), $flush = false ) {
9610 $this->_logger->entrance();
9611
9612 $site = $this->send_install_update( $override, $flush );
9613
9614 if ( false === $site ) {
9615 // No sync required.
9616 return;
9617 }
9618
9619 if ( ! $this->is_api_result_entity( $site ) ) {
9620 // Failed to sync, don't update locally.
9621 return;
9622 }
9623
9624 $this->_site = new FS_Site( $site );
9625
9626 $this->_store_site( true );
9627 }
9628
9629 /**
9630 * Update install only if changed.
9631 *
9632 * @author Vova Feldman (@svovaf)
9633 * @since 1.0.9
9634 *
9635 * @param string[] string $override
9636 * @param bool $flush
9637 */
9638 private function sync_installs( $override = array(), $flush = false ) {
9639 $this->_logger->entrance();
9640
9641 $result = $this->send_installs_update( $override, $flush );
9642
9643 if ( false === $result ) {
9644 // No sync required.
9645 return;
9646 }
9647
9648 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9649 // Failed to sync, don't update locally.
9650 return;
9651 }
9652
9653 $address_to_blog_map = $this->get_address_to_blog_map();
9654
9655 foreach ( $result->installs as $install ) {
9656 $this->_site = new FS_Site( $install );
9657
9658 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9659 $blog_id = $address_to_blog_map[ $address ];
9660
9661 $this->_store_site( true, $blog_id );
9662 }
9663 }
9664
9665 /**
9666 * Track install's custom event.
9667 *
9668 * IMPORTANT:
9669 * Custom event tracking is currently only supported for specific clients.
9670 * If you are not one of them, please don't use this method. If you will,
9671 * the API will simply ignore your request based on the plugin ID.
9672 *
9673 * Need custom tracking for your plugin or theme?
9674 * If you are interested in custom event tracking please contact yo@freemius.com
9675 * for further details.
9676 *
9677 * @author Vova Feldman (@svovaf)
9678 * @since 1.2.1
9679 *
9680 * @param string $name Event name.
9681 * @param array $properties Associative key/value array with primitive values only
9682 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9683 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9684 *
9685 * @return object|false Event data or FALSE on failure.
9686 *
9687 * @throws \Freemius_InvalidArgumentException
9688 */
9689 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9690 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9691
9692 if ( ! $this->is_registered() ) {
9693 return false;
9694 }
9695
9696 $event = array( 'type' => $name );
9697
9698 if ( is_numeric( $process_at ) && $process_at > time() ) {
9699 $event['process_at'] = $process_at;
9700 }
9701
9702 if ( $once ) {
9703 $event['once'] = true;
9704 }
9705
9706 if ( ! empty( $properties ) ) {
9707 // Verify associative array values are primitive.
9708 foreach ( $properties as $k => $v ) {
9709 if ( ! is_scalar( $v ) ) {
9710 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9711 }
9712 }
9713
9714 $event['properties'] = $properties;
9715 }
9716
9717 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9718
9719 return $this->is_api_error( $result ) ?
9720 false :
9721 $result;
9722 }
9723
9724 /**
9725 * Track install's custom event only once, but it still triggers the API call.
9726 *
9727 * IMPORTANT:
9728 * Custom event tracking is currently only supported for specific clients.
9729 * If you are not one of them, please don't use this method. If you will,
9730 * the API will simply ignore your request based on the plugin ID.
9731 *
9732 * Need custom tracking for your plugin or theme?
9733 * If you are interested in custom event tracking please contact yo@freemius.com
9734 * for further details.
9735 *
9736 * @author Vova Feldman (@svovaf)
9737 * @since 1.2.1
9738 *
9739 * @param string $name Event name.
9740 * @param array $properties Associative key/value array with primitive values only
9741 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9742 *
9743 * @return object|false Event data or FALSE on failure.
9744 *
9745 * @throws \Freemius_InvalidArgumentException
9746 *
9747 * @user Freemius::track_event()
9748 */
9749 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9750 return $this->track_event( $name, $properties, $process_at, true );
9751 }
9752
9753 /**
9754 * Plugin uninstall hook.
9755 *
9756 * @author Vova Feldman (@svovaf)
9757 * @since 1.0.1
9758 *
9759 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9760 */
9761 function _uninstall_plugin_event( $check_user = true ) {
9762 $this->_logger->entrance( 'slug = ' . $this->_slug );
9763
9764 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9765 return;
9766 }
9767
9768 $params = array();
9769 $uninstall_reason = null;
9770 if ( isset( $this->_storage->uninstall_reason ) ) {
9771 $uninstall_reason = $this->_storage->uninstall_reason;
9772 $params['reason_id'] = $uninstall_reason->id;
9773 $params['reason_info'] = $uninstall_reason->info;
9774 }
9775
9776 if ( ! $this->is_registered() ) {
9777 // Send anonymous uninstall event only if user submitted a feedback.
9778 if ( isset( $uninstall_reason ) ) {
9779 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9780 $this->opt_in( false, false, false, false, true );
9781 } else {
9782 $params['uid'] = $this->get_anonymous_id();
9783 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9784 }
9785 }
9786 } else {
9787 $params = array_merge( $params, array(
9788 'is_active' => false,
9789 'is_uninstalled' => true,
9790 ) );
9791
9792 if ( $this->_is_network_active ) {
9793 // Send uninstall event.
9794 $this->send_installs_update( $params );
9795 } else {
9796 // Send uninstall event.
9797 $this->send_install_update( $params );
9798 }
9799 }
9800
9801 // @todo Decide if we want to delete plugin information from db.
9802 }
9803
9804 /**
9805 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9806 *
9807 * @author Vova Feldman (@svovaf)
9808 * @since 2.2.1
9809 *
9810 * @param string $is_premium
9811 * @param string $caller
9812 *
9813 * @return string
9814 */
9815 function set_basename( $is_premium, $caller ) {
9816 $basename = plugin_basename( $caller );
9817
9818 $current_basename = $is_premium ?
9819 $this->_premium_plugin_basename :
9820 $this->_free_plugin_basename;
9821
9822 if ( $current_basename == $basename ) {
9823 // Basename value set correctly.
9824 return;
9825 }
9826
9827 if ( $is_premium ) {
9828 $this->_premium_plugin_basename = $basename;
9829 } else {
9830 $this->_free_plugin_basename = $basename;
9831 }
9832
9833 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9834
9835 register_activation_hook(
9836 $plugin_dir . $basename,
9837 array( &$this, '_activate_plugin_event_hook' )
9838 );
9839 }
9840
9841 /**
9842 * @author Vova Feldman (@svovaf)
9843 * @since 1.1.1
9844 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9845 *
9846 * @return string
9847 */
9848 function premium_plugin_basename() {
9849 if ( ! isset( $this->_premium_plugin_basename ) ) {
9850 $this->_premium_plugin_basename = $this->is_premium() ?
9851 // The product is premium, so use the current basename.
9852 $this->_plugin_basename :
9853 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9854 }
9855
9856 return $this->_premium_plugin_basename;
9857 }
9858
9859 /**
9860 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9861 *
9862 * @author Vova Feldman (@svovaf)
9863 * @since 1.0.2
9864 */
9865 public static function _uninstall_plugin_hook() {
9866 self::_load_required_static();
9867
9868 self::$_static_logger->entrance();
9869
9870 if ( ! current_user_can( 'activate_plugins' ) ) {
9871 return;
9872 }
9873
9874 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9875
9876 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9877
9878 define( 'WP_FS__UNINSTALL_MODE', true );
9879
9880 $fs = self::get_instance_by_file( $plugin_file );
9881
9882 if ( is_object( $fs ) ) {
9883 $fs->remove_sdk_reference();
9884
9885 self::require_plugin_essentials();
9886
9887 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9888 is_plugin_active( $fs->premium_plugin_basename() )
9889 ) {
9890 // Deleting Free or Premium plugin version while the other version still installed.
9891 return;
9892 }
9893
9894 $fs->_uninstall_plugin_event();
9895
9896 $fs->do_action( 'after_uninstall' );
9897 }
9898 }
9899
9900 #----------------------------------------------------------------------------------
9901 #region Plugin Information
9902 #----------------------------------------------------------------------------------
9903
9904 /**
9905 * Load WordPress core plugin.php essential module.
9906 *
9907 * @author Vova Feldman (@svovaf)
9908 * @since 1.1.1
9909 */
9910 private static function require_plugin_essentials() {
9911 if ( ! function_exists( 'get_plugins' ) ) {
9912 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9913
9914 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9915 }
9916 }
9917
9918 /**
9919 * Load WordPress core pluggable.php module.
9920 *
9921 * @author Vova Feldman (@svovaf)
9922 * @since 1.1.2
9923 */
9924 private static function require_pluggable_essentials() {
9925 if ( ! function_exists( 'wp_get_current_user' ) ) {
9926 require_once ABSPATH . 'wp-includes/pluggable.php';
9927 }
9928 }
9929
9930 /**
9931 * Return plugin data.
9932 *
9933 * @author Vova Feldman (@svovaf)
9934 * @since 1.0.1
9935 *
9936 * @param bool $reparse_plugin_metadata
9937 *
9938 * @return array
9939 */
9940 function get_plugin_data( $reparse_plugin_metadata = false ) {
9941 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9942 self::require_plugin_essentials();
9943
9944 if ( $this->is_plugin() ) {
9945 /**
9946 * @author Vova Feldman (@svovaf)
9947 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9948 *
9949 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9950 */
9951 $plugin_data = get_plugin_data(
9952 $this->_plugin_main_file_path,
9953 false,
9954 false
9955 );
9956 } else {
9957 $theme_data = wp_get_theme();
9958
9959 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9960 $parent_theme = $theme_data->parent();
9961
9962 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9963 $theme_data = $parent_theme;
9964 }
9965 }
9966
9967 $plugin_data = array(
9968 'Name' => $theme_data->get( 'Name' ),
9969 'Version' => $theme_data->get( 'Version' ),
9970 'Author' => $theme_data->get( 'Author' ),
9971 'Description' => $theme_data->get( 'Description' ),
9972 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9973 );
9974 }
9975
9976 $this->_plugin_data = $plugin_data;
9977 }
9978
9979 return $this->_plugin_data;
9980 }
9981
9982 /**
9983 * @author Vova Feldman (@svovaf)
9984 * @since 1.0.1
9985 * @since 1.2.2.5 If slug not set load slug by module ID.
9986 *
9987 * @return string Plugin slug.
9988 */
9989 function get_slug() {
9990 if ( ! isset( $this->_slug ) ) {
9991 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9992 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9993 }
9994
9995 return $this->_slug;
9996 }
9997
9998 /**
9999 * @author Leo Fajardo (@leorw)
10000 * @since 2.2.1
10001 *
10002 * @return string
10003 */
10004 function get_premium_slug() {
10005 return is_object( $this->_plugin ) ?
10006 $this->_plugin->premium_slug :
10007 "{$this->_slug}-premium";
10008 }
10009
10010 /**
10011 * Retrieve the desired folder name for the product.
10012 *
10013 * @author Vova Feldman (@svovaf)
10014 * @since 1.2.1.7
10015 *
10016 * @return string Plugin slug.
10017 */
10018 function get_target_folder_name() {
10019 return $this->can_use_premium_code() ?
10020 $this->_plugin->premium_slug :
10021 $this->_slug;
10022 }
10023
10024 /**
10025 * @author Vova Feldman (@svovaf)
10026 * @since 1.0.1
10027 *
10028 * @return number Plugin ID.
10029 */
10030 function get_id() {
10031 return $this->_plugin->id;
10032 }
10033
10034 /**
10035 * @author Leo Fajardo (@leorw)
10036 * @since 2.2.4
10037 *
10038 * @return number|null Bundle ID.
10039 */
10040 function get_bundle_id() {
10041 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10042 $this->_plugin->bundle_id :
10043 null;
10044 }
10045
10046 /**
10047 * @author Vova Feldman (@svovaf)
10048 * @since 2.3.1
10049 *
10050 * @return string|null Bundle public key.
10051 */
10052 function get_bundle_public_key() {
10053 return isset( $this->_plugin->bundle_public_key ) ?
10054 $this->_plugin->bundle_public_key :
10055 null;
10056 }
10057
10058 /**
10059 * @author Vova Feldman (@svovaf)
10060 * @since 1.2.1.5
10061 *
10062 * @return string Freemius SDK version
10063 */
10064 function get_sdk_version() {
10065 return $this->version;
10066 }
10067
10068 /**
10069 * @author Vova Feldman (@svovaf)
10070 * @since 1.2.1.5
10071 *
10072 * @return number Parent plugin ID (if parent exist).
10073 */
10074 function get_parent_id() {
10075 return $this->is_addon() ?
10076 $this->get_parent_instance()->get_id() :
10077 $this->_plugin->id;
10078 }
10079
10080 /**
10081 * @author Vova Feldman (@svovaf)
10082 * @since 2.3.1
10083 *
10084 * @return string
10085 */
10086 function get_usage_tracking_terms_url() {
10087 return $this->apply_filters(
10088 'usage_tracking_terms_url',
10089 "https://freemius.com/wordpress/usage-tracking/{$this->_plugin->id}/{$this->_slug}/"
10090 );
10091 }
10092
10093 /**
10094 * @author Vova Feldman (@svovaf)
10095 * @since 2.3.1
10096 *
10097 * @return string
10098 */
10099 function get_eula_url() {
10100 return $this->apply_filters(
10101 'eula_url',
10102 "https://freemius.com/terms/{$this->_plugin->id}/{$this->_slug}/"
10103 );
10104 }
10105
10106 /**
10107 * @author Vova Feldman (@svovaf)
10108 * @since 1.0.1
10109 *
10110 * @return string Plugin public key.
10111 */
10112 function get_public_key() {
10113 return $this->_plugin->public_key;
10114 }
10115
10116 /**
10117 * Will be available only on sandbox mode.
10118 *
10119 * @author Vova Feldman (@svovaf)
10120 * @since 1.0.4
10121 *
10122 * @return mixed Plugin secret key.
10123 */
10124 function get_secret_key() {
10125 return $this->_plugin->secret_key;
10126 }
10127
10128 /**
10129 * @author Vova Feldman (@svovaf)
10130 * @since 1.1.1
10131 *
10132 * @return bool
10133 */
10134 function has_secret_key() {
10135 return ! empty( $this->_plugin->secret_key );
10136 }
10137
10138 /**
10139 * @author Vova Feldman (@svovaf)
10140 * @since 1.0.9
10141 *
10142 * @param string|bool $premium_suffix
10143 *
10144 * @return string
10145 */
10146 function get_plugin_name( $premium_suffix = false ) {
10147 $this->_logger->entrance();
10148
10149 /**
10150 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10151 *
10152 * @author Vova Feldman
10153 */
10154 if ( ! isset( $this->_plugin_name ) ) {
10155 // Name is not yet set.
10156 $this->set_name( $premium_suffix );
10157 } else if (
10158 ! empty( $premium_suffix ) &&
10159 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10160 ) {
10161 // Name is already set, but there's a change in the premium suffix.
10162 $this->set_name( $premium_suffix );
10163 }
10164
10165 return $this->_plugin_name;
10166 }
10167
10168 /**
10169 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10170 *
10171 * @author Vova Feldman (@svovaf)
10172 * @since 2.2.1
10173 *
10174 * @param string $premium_suffix
10175 */
10176 private function set_name( $premium_suffix = '' ) {
10177 $plugin_data = $this->get_plugin_data();
10178
10179 // Get name.
10180 $this->_plugin_name = $plugin_data['Name'];
10181
10182 if ( is_string( $premium_suffix ) ) {
10183 $premium_suffix = trim( $premium_suffix );
10184
10185 if ( ! empty( $premium_suffix ) ) {
10186 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10187 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10188 $suffix_len = strlen( $suffix );
10189
10190 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10191 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10192 ) {
10193 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10194 }
10195 }
10196 }
10197
10198 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10199 }
10200
10201 /**
10202 * @author Vova Feldman (@svovaf)
10203 * @since 1.0.0
10204 *
10205 * @param bool $reparse_plugin_metadata
10206 *
10207 * @return string
10208 */
10209 function get_plugin_version( $reparse_plugin_metadata = false ) {
10210 $this->_logger->entrance();
10211
10212 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10213
10214 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10215
10216 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10217 }
10218
10219 /**
10220 * @author Vova Feldman (@svovaf)
10221 * @since 1.2.1.7
10222 *
10223 * @return string
10224 */
10225 function get_plugin_title() {
10226 $this->_logger->entrance();
10227
10228 $title = $this->_plugin->title;
10229
10230 return $this->apply_filters( 'plugin_title', $title );
10231 }
10232
10233 /**
10234 * @author Vova Feldman (@svovaf)
10235 * @since 1.2.2.7
10236 *
10237 * @param bool $lowercase
10238 *
10239 * @return string
10240 */
10241 function get_module_label( $lowercase = false ) {
10242 $label = $this->is_addon() ?
10243 $this->get_text_inline( 'Add-On', 'addon' ) :
10244 ( $this->is_plugin() ?
10245 $this->get_text_inline( 'Plugin', 'plugin' ) :
10246 $this->get_text_inline( 'Theme', 'theme' ) );
10247
10248 if ( $lowercase ) {
10249 $label = strtolower( $label );
10250 }
10251
10252 return $label;
10253 }
10254
10255 /**
10256 * @author Vova Feldman (@svovaf)
10257 * @since 1.0.4
10258 *
10259 * @return string
10260 */
10261 function get_plugin_basename() {
10262 if ( ! isset( $this->_plugin_basename ) ) {
10263 if ( $this->is_plugin() ) {
10264 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10265 } else {
10266 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10267 }
10268 }
10269
10270 return $this->_plugin_basename;
10271 }
10272
10273 function get_plugin_folder_name() {
10274 $this->_logger->entrance();
10275
10276 $plugin_folder = $this->_plugin_basename;
10277
10278 while ( '.' !== dirname( $plugin_folder ) ) {
10279 $plugin_folder = dirname( $plugin_folder );
10280 }
10281
10282 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10283
10284 return $plugin_folder;
10285 }
10286
10287 #endregion ------------------------------------------------------------------
10288
10289 /* Account
10290 ------------------------------------------------------------------------------------------------------------------*/
10291
10292 /**
10293 * Find plugin's slug by plugin's basename.
10294 *
10295 * @author Vova Feldman (@svovaf)
10296 * @since 1.0.9
10297 *
10298 * @param string $plugin_base_name
10299 *
10300 * @return false|string
10301 */
10302 private static function find_slug_by_basename( $plugin_base_name ) {
10303 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10304
10305 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10306 return false;
10307 }
10308
10309 return $file_slug_map[ $plugin_base_name ];
10310 }
10311
10312 /**
10313 * Store the map between the plugin's basename to the slug.
10314 *
10315 * @author Vova Feldman (@svovaf)
10316 * @since 1.0.9
10317 */
10318 private function store_file_slug_map() {
10319 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10320
10321 if ( ! array( $file_slug_map ) ) {
10322 $file_slug_map = array();
10323 }
10324
10325 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10326 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10327 ) {
10328 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10329 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10330 }
10331 }
10332
10333 /**
10334 * @return array[number]FS_User
10335 */
10336 static function get_all_users() {
10337 $users = self::maybe_get_entities_account_option( 'users', array() );
10338
10339 if ( ! is_array( $users ) ) {
10340 $users = array();
10341 }
10342
10343 return $users;
10344 }
10345
10346 /**
10347 * @param string $module_type
10348 * @param null|int $blog_id Since 2.0.0
10349 *
10350 * @return array[string]FS_Site
10351 */
10352 private static function get_all_sites(
10353 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10354 $blog_id = null
10355 ) {
10356 $sites = self::get_account_option( 'sites', $module_type, $blog_id );
10357
10358 if ( ! is_array( $sites ) ) {
10359 $sites = array();
10360 }
10361
10362 return $sites;
10363 }
10364
10365 /**
10366 * @author Leo Fajardo (@leorw)
10367 *
10368 * @since 1.2.2
10369 *
10370 * @param string $option_name
10371 * @param string $module_type
10372 * @param null|int $network_level_or_blog_id Since 2.0.0
10373 *
10374 * @return mixed
10375 */
10376 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10377 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10378 $option_name = $module_type . '_' . $option_name;
10379 }
10380
10381 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10382 }
10383
10384 /**
10385 * @author Leo Fajardo (@leorw)
10386 *
10387 * @since 1.2.2
10388 *
10389 * @param string $option_name
10390 * @param mixed $option_value
10391 * @param bool $store
10392 * @param null|int $network_level_or_blog_id Since 2.0.0
10393 */
10394 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10395 self::set_account_option_by_module(
10396 $this->_module_type,
10397 $option_name,
10398 $option_value,
10399 $store,
10400 $network_level_or_blog_id
10401 );
10402 }
10403
10404 /**
10405 * @author Vova Feldman (@svovaf)
10406 *
10407 * @since 1.2.2.7
10408 *
10409 * @param string $module_type
10410 * @param string $option_name
10411 * @param mixed $option_value
10412 * @param bool $store
10413 * @param null|int $network_level_or_blog_id Since 2.0.0
10414 */
10415 private static function set_account_option_by_module(
10416 $module_type,
10417 $option_name,
10418 $option_value,
10419 $store,
10420 $network_level_or_blog_id = null
10421 ) {
10422 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10423 $option_name = $module_type . '_' . $option_name;
10424 }
10425
10426 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10427 }
10428
10429 /**
10430 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10431 *
10432 * @author Leo Fajardo (@leorw)
10433 * @since 2.3.1
10434 *
10435 * @param string $option_name
10436 * @param mixed $default
10437 * @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).
10438 *
10439 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10440 */
10441 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10442 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10443
10444 $class_name = '';
10445
10446 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10447 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10448 }
10449
10450 switch ( $option_name ) {
10451 case 'plugins':
10452 case 'themes':
10453 case 'addons':
10454 $class_name = FS_Plugin::get_class_name();
10455 break;
10456 case 'users':
10457 $class_name = FS_User::get_class_name();
10458 break;
10459 case 'sites':
10460 $class_name = FS_Site::get_class_name();
10461 break;
10462 case 'licenses':
10463 case 'all_licenses':
10464 $class_name = FS_Plugin_License::get_class_name();
10465 break;
10466 case 'plans':
10467 $class_name = FS_Plugin_Plan::get_class_name();
10468 break;
10469 case 'updates':
10470 $class_name = FS_Plugin_Tag::get_class_name();
10471 break;
10472 }
10473
10474 if ( empty( $class_name ) ) {
10475 return $option;
10476 }
10477
10478 return fs_get_entities( $option, $class_name );
10479 }
10480
10481 /**
10482 * @author Vova Feldman (@svovaf)
10483 * @since 1.0.6
10484 *
10485 * @param number|null $module_id
10486 *
10487 * @return FS_Plugin_License[]
10488 */
10489 private static function get_all_licenses( $module_id = null ) {
10490 $licenses = self::get_account_option( 'all_licenses' );
10491
10492 if ( ! is_array( $licenses ) ) {
10493 $licenses = array();
10494 }
10495
10496 if ( is_null( $module_id ) ) {
10497 return $licenses;
10498 }
10499
10500 $licenses = isset( $licenses[ $module_id ] ) ?
10501 $licenses[ $module_id ] :
10502 array();
10503
10504 return $licenses;
10505 }
10506
10507 /**
10508 * @author Leo Fajardo (@leorw)
10509 * @since 2.0.0
10510 *
10511 * @return array
10512 */
10513 private static function get_all_licenses_by_module_type() {
10514 $licenses = self::get_account_option( 'all_licenses' );
10515
10516 $licenses_by_module_type = array(
10517 WP_FS__MODULE_TYPE_PLUGIN => array(),
10518 WP_FS__MODULE_TYPE_THEME => array()
10519 );
10520
10521 if ( ! is_array( $licenses ) ) {
10522 return $licenses_by_module_type;
10523 }
10524
10525 foreach ( $licenses as $module_id => $module_licenses ) {
10526 $fs = self::get_instance_by_id( $module_id );
10527 if ( false === $fs ) {
10528 continue;
10529 }
10530
10531 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10532 }
10533
10534 return $licenses_by_module_type;
10535 }
10536
10537 /**
10538 * @author Leo Fajardo (@leorw)
10539 * @since 2.0.0
10540 *
10541 * @param number $module_id
10542 * @param number|null $user_id
10543 *
10544 * @return array
10545 */
10546 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10547 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10548
10549 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10550 $all_modules_user_id_license_ids_map = array();
10551 }
10552
10553 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10554 $all_modules_user_id_license_ids_map[ $module_id ] :
10555 array();
10556
10557 if ( FS_User::is_valid_id( $user_id ) ) {
10558 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10559 $user_id_license_ids_map[ $user_id ] :
10560 array();
10561 }
10562
10563 return $user_id_license_ids_map;
10564 }
10565
10566 /**
10567 * @author Leo Fajardo (@leorw)
10568 * @since 2.0.0
10569 *
10570 * @param array $new_user_id_license_ids_map
10571 * @param number $module_id
10572 * @param number|null $user_id
10573 */
10574 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10575 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10576 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10577 $all_modules_user_id_license_ids_map = array();
10578 }
10579
10580 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10581 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10582 }
10583
10584 if ( FS_User::is_valid_id( $user_id ) ) {
10585 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10586 } else {
10587 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10588 }
10589
10590 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10591 }
10592
10593 /**
10594 * Get a collection of the user's linked license IDs.
10595 *
10596 * @author Vova Feldman (@svovaf)
10597 * @since 2.0.0
10598 *
10599 * @param number $user_id
10600 *
10601 * @return number[]
10602 */
10603 private function get_user_linked_license_ids( $user_id ) {
10604 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10605 }
10606
10607 /**
10608 * Override the user's linked license IDs with a new IDs collection.
10609 *
10610 * @author Vova Feldman (@svovaf)
10611 * @since 2.0.0
10612 *
10613 * @param number $user_id
10614 * @param number[] $license_ids
10615 */
10616 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10617 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10618 }
10619
10620 /**
10621 * Link a specified license ID to a given user.
10622 *
10623 * @author Vova Feldman (@svovaf)
10624 * @since 2.0.0
10625 *
10626 * @param number $license_id
10627 * @param number $user_id
10628 */
10629 private function link_license_2_user( $license_id, $user_id ) {
10630 $license_ids = $this->get_user_linked_license_ids( $user_id );
10631
10632 if ( in_array( $license_id, $license_ids ) ) {
10633 // License already linked.
10634 return;
10635 }
10636
10637 $license_ids[] = $license_id;
10638
10639 $this->set_user_linked_license_ids( $user_id, $license_ids );
10640 }
10641
10642 /**
10643 * @param string|bool $module_type
10644 *
10645 * @return FS_Plugin_Plan[]
10646 */
10647 private static function get_all_plans( $module_type = false ) {
10648 $plans = self::get_account_option( 'plans', $module_type );
10649
10650 if ( ! is_array( $plans ) ) {
10651 $plans = array();
10652 }
10653
10654 return $plans;
10655 }
10656
10657 /**
10658 * @author Vova Feldman (@svovaf)
10659 * @since 1.0.4
10660 *
10661 * @return FS_Plugin_Tag[]
10662 */
10663 private static function get_all_updates() {
10664 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10665
10666 if ( ! is_array( $updates ) ) {
10667 $updates = array();
10668 }
10669
10670 return $updates;
10671 }
10672
10673 /**
10674 * @author Vova Feldman (@svovaf)
10675 * @since 1.0.6
10676 *
10677 * @return array<number,FS_Plugin[]>|false
10678 */
10679 private static function get_all_addons() {
10680 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10681
10682 if ( ! is_array( $addons ) ) {
10683 $addons = array();
10684 }
10685
10686 return $addons;
10687 }
10688
10689 /**
10690 * @author Vova Feldman (@svovaf)
10691 * @since 1.0.6
10692 *
10693 * @return number[]|false
10694 */
10695 private static function get_all_account_addons() {
10696 $addons = self::$_accounts->get_option( 'account_addons', array() );
10697
10698 if ( ! is_array( $addons ) ) {
10699 $addons = array();
10700 }
10701
10702 return $addons;
10703 }
10704
10705 /**
10706 * Check if user has connected his account (opted-in).
10707 *
10708 * Note:
10709 * If the user opted-in and opted-out on a later stage,
10710 * this will still return true. If you want to check if the
10711 * user is currently opted-in, use:
10712 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10713 *
10714 * @author Vova Feldman (@svovaf)
10715 * @since 1.0.1
10716 * @return bool
10717 */
10718 function is_registered() {
10719 return is_object( $this->_user );
10720 }
10721
10722 /**
10723 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10724 *
10725 * @author Leo Fajardo (@leorw)
10726 * @since 1.2.1.5
10727 *
10728 * @return bool
10729 */
10730 function is_tracking_allowed() {
10731 return ( is_object( $this->_site ) && $this->_site->is_tracking_allowed() );
10732 }
10733
10734 /**
10735 * @author Leo Fajardo (@leorw)
10736 * @since 2.4.0
10737 *
10738 * @return bool
10739 */
10740 function is_bundle_license_auto_activation_enabled() {
10741 return $this->is_addon() ?
10742 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10743 $this->_is_bundle_license_auto_activation_enabled;
10744 }
10745
10746 /**
10747 * @author Vova Feldman (@svovaf)
10748 * @since 1.0.4
10749 *
10750 * @return FS_Plugin
10751 */
10752 function get_plugin() {
10753 return $this->_plugin;
10754 }
10755
10756 /**
10757 * @author Vova Feldman (@svovaf)
10758 * @since 1.0.3
10759 *
10760 * @return FS_User
10761 */
10762 function get_user() {
10763 return $this->_user;
10764 }
10765
10766 /**
10767 * @author Vova Feldman (@svovaf)
10768 * @since 1.0.3
10769 *
10770 * @return FS_Site
10771 */
10772 function get_site() {
10773 return $this->_site;
10774 }
10775
10776 /**
10777 * Get plugin add-ons.
10778 *
10779 * @author Vova Feldman (@svovaf)
10780 * @since 1.0.6
10781 *
10782 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10783 *
10784 * @param bool $flush
10785 *
10786 * @return FS_Plugin[]|false
10787 */
10788 function get_addons( $flush = false ) {
10789 $this->_logger->entrance();
10790
10791 if ( ! $this->_has_addons ) {
10792 return false;
10793 }
10794
10795 $addons = $this->sync_addons( $flush );
10796
10797 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10798 false :
10799 $addons;
10800 }
10801
10802 /**
10803 * @author Vova Feldman (@svovaf)
10804 * @since 1.0.6
10805 *
10806 * @return number[]|false
10807 */
10808 function get_account_addons() {
10809 $this->_logger->entrance();
10810
10811 $addons = self::get_all_account_addons();
10812
10813 if ( ! is_array( $addons ) ||
10814 ! isset( $addons[ $this->_plugin->id ] ) ||
10815 ! is_array( $addons[ $this->_plugin->id ] ) ||
10816 0 === count( $addons[ $this->_plugin->id ] )
10817 ) {
10818 return false;
10819 }
10820
10821 return $addons[ $this->_plugin->id ];
10822 }
10823
10824 /**
10825 * Check if user has any
10826 *
10827 * @author Vova Feldman (@svovaf)
10828 * @since 1.1.6
10829 *
10830 * @return bool
10831 */
10832 function has_account_addons() {
10833 $addons = $this->get_account_addons();
10834
10835 return is_array( $addons ) && ( 0 < count( $addons ) );
10836 }
10837
10838
10839 /**
10840 * Get add-on by ID (from local data).
10841 *
10842 * @author Vova Feldman (@svovaf)
10843 * @since 1.0.6
10844 *
10845 * @param number $id
10846 *
10847 * @return FS_Plugin|false
10848 */
10849 function get_addon( $id ) {
10850 $this->_logger->entrance();
10851
10852 $addons = $this->get_addons();
10853
10854 if ( is_array( $addons ) ) {
10855 foreach ( $addons as $addon ) {
10856 if ( $id == $addon->id ) {
10857 return $addon;
10858 }
10859 }
10860 }
10861
10862 return false;
10863 }
10864
10865 /**
10866 * Get add-on by slug (from local data).
10867 *
10868 * @author Vova Feldman (@svovaf)
10869 * @since 1.0.6
10870 *
10871 * @param string $slug
10872 *
10873 * @param bool $flush
10874 *
10875 * @return FS_Plugin|false
10876 */
10877 function get_addon_by_slug( $slug, $flush = false ) {
10878 $this->_logger->entrance();
10879
10880 $addons = $this->get_addons( $flush );
10881
10882 if ( is_array( $addons ) ) {
10883 foreach ( $addons as $addon ) {
10884 if ( $slug === $addon->slug ) {
10885 return $addon;
10886 }
10887 }
10888 }
10889
10890 return false;
10891 }
10892
10893 /**
10894 * @var array<number,object[]> {
10895 * @key number Add-on ID.
10896 * @val object[] The add-on's plans and prices object.
10897 * }
10898 */
10899 private $plans_and_pricing_by_addon_id;
10900
10901 /**
10902 * @author Leo Fajardo (@leorw)
10903 * @since 2.3.0
10904 *
10905 * @return array<number,object[]> {
10906 * @key number Add-on ID.
10907 * @val object[] The add-on's plans and prices object.
10908 * }
10909 */
10910 function _get_addons_plans_and_pricing_map_by_id() {
10911 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
10912 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
10913
10914 $plans_and_pricing_by_addon_id = array();
10915 if ( $this->is_api_result_object( $result, 'addons' ) ) {
10916 foreach ( $result->addons as $addon ) {
10917 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
10918 }
10919 }
10920
10921 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
10922 }
10923
10924 return $this->plans_and_pricing_by_addon_id;
10925 }
10926
10927 /**
10928 * @author Leo Fajardo (@leorw)
10929 * @since 2.3.0
10930 *
10931 * @param number $addon_id
10932 * @param bool $is_installed
10933 *
10934 * @return array
10935 */
10936 function _get_addon_info( $addon_id, $is_installed ) {
10937 $addon = $this->get_addon( $addon_id );
10938
10939 if ( ! is_object( $addon ) ) {
10940 // Unexpected call.
10941 return array();
10942 }
10943
10944 $slug = $addon->slug;
10945
10946 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
10947
10948 if ( ! fs_is_network_admin() ) {
10949 // Get blog-level activated installations.
10950 $sites = self::maybe_get_entities_account_option( 'sites', array() );
10951 } else {
10952 $sites = null;
10953
10954 if ( $this->is_addon_activated( $addon_id ) &&
10955 $this->get_addon_instance( $addon_id )->is_network_active()
10956 ) {
10957 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
10958 // Get network-level activated installations.
10959 $sites = self::maybe_get_entities_account_option(
10960 'sites',
10961 array(),
10962 $addon_storage->network_install_blog_id
10963 );
10964 }
10965 }
10966 }
10967
10968 $addon_info = array(
10969 'is_connected' => false,
10970 'slug' => $slug,
10971 'title' => $addon->title,
10972 'is_whitelabeled' => $addon_storage->is_whitelabeled
10973 );
10974
10975 if ( ! $is_installed ) {
10976 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
10977
10978 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
10979 $has_paid_plan = false;
10980 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
10981
10982 if ( is_array( $plans ) && count( $plans ) > 0 ) {
10983 foreach ( $plans as $plan ) {
10984 if ( isset( $plan->pricing ) &&
10985 is_array( $plan->pricing ) &&
10986 count( $plan->pricing ) > 0
10987 ) {
10988 $has_paid_plan = true;
10989 break;
10990 }
10991 }
10992 }
10993
10994 $addon_info['has_paid_plan'] = $has_paid_plan;
10995 }
10996 }
10997
10998 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
10999 return $addon_info;
11000 }
11001
11002 $site = $sites[ $slug ];
11003
11004 $addon_info['is_connected'] = (
11005 ( $addon->parent_plugin_id == $this->get_id() ) &&
11006 is_object( $site ) &&
11007 FS_Site::is_valid_id( $site->id ) &&
11008 FS_User::is_valid_id( $site->user_id ) &&
11009 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11010 );
11011
11012 if ( $addon_info['is_connected'] && $is_installed ) {
11013 return $addon_info;
11014 }
11015
11016 $addon_info['site'] = $site;
11017
11018 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11019 if ( isset( $plugins_data[ $slug ] ) ) {
11020 $plugin_data = $plugins_data[ $slug ];
11021
11022 $addon_info['version'] = $plugin_data->version;
11023 }
11024
11025 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11026 if ( isset( $all_plans[ $slug ] ) ) {
11027 $plans = $all_plans[ $slug ];
11028
11029 foreach ( $plans as $plan ) {
11030 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11031 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11032 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11033 break;
11034 }
11035 }
11036 }
11037
11038 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11039 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11040 foreach ( $licenses[ $addon_id ] as $license ) {
11041 if ( $license->id == $site->license_id ) {
11042 $addon_info['license'] = $license;
11043 break;
11044 }
11045 }
11046 }
11047
11048 if ( isset( $addon_info['license'] ) ) {
11049 if ( isset( $addon_storage->subscriptions ) &&
11050 ! empty( $addon_storage->subscriptions )
11051 ) {
11052 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11053
11054 foreach ( $addon_subscriptions as $subscription ) {
11055 if ( $subscription->license_id == $site->license_id ) {
11056 $addon_info['subscription'] = $subscription;
11057 break;
11058 }
11059 }
11060 }
11061 }
11062
11063 return $addon_info;
11064 }
11065
11066 /**
11067 * @author Vova Feldman (@svovaf)
11068 * @since 2.0.0
11069 *
11070 * @param number $user_id
11071 *
11072 * @return FS_User
11073 */
11074 static function _get_user_by_id( $user_id ) {
11075 self::$_static_logger->entrance( "user_id = {$user_id}" );
11076
11077 $users = self::get_all_users();
11078
11079 if ( is_array( $users ) ) {
11080 if ( isset( $users[ $user_id ] ) &&
11081 $users[ $user_id ] instanceof FS_User &&
11082 $user_id == $users[ $user_id ]->id
11083 ) {
11084 return $users[ $user_id ];
11085 }
11086
11087 // If user wasn't found by the key, iterate over all the users collection.
11088 foreach ( $users as $user ) {
11089 /**
11090 * @var FS_User $user
11091 */
11092 if ( $user_id == $user->id ) {
11093 return $user;
11094 }
11095 }
11096 }
11097
11098 return null;
11099 }
11100
11101 /**
11102 * Checks if a Freemius user_id is associated with a super-admin.
11103 *
11104 * @author Vova Feldman (@svovaf)
11105 * @since 2.0.0
11106 *
11107 * @param number $user_id
11108 *
11109 * @return bool
11110 */
11111 private static function is_super_admin( $user_id ) {
11112 $is_super_admin = false;
11113
11114 $user = self::_get_user_by_id( $user_id );
11115
11116 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11117 self::require_pluggable_essentials();
11118
11119 $wp_user = get_user_by( 'email', $user->email );
11120
11121 if ( $wp_user instanceof WP_User ) {
11122 $super_admins = get_super_admins();
11123 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11124 }
11125 }
11126
11127 return $is_super_admin;
11128 }
11129
11130 #----------------------------------------------------------------------------------
11131 #region Plans & Licensing
11132 #----------------------------------------------------------------------------------
11133
11134 /**
11135 * Check if running premium plugin code.
11136 *
11137 * @author Vova Feldman (@svovaf)
11138 * @since 1.0.5
11139 *
11140 * @return bool
11141 */
11142 function is_premium() {
11143 /**
11144 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11145 * `register_constructor_hooks` method.
11146 *
11147 * @author Leo Fajardo (@leorw)
11148 * @since 2.2.3
11149 */
11150 return is_object( $this->_plugin ) ?
11151 $this->_plugin->is_premium :
11152 false;
11153 }
11154
11155 /**
11156 * Get site's plan ID.
11157 *
11158 * @author Vova Feldman (@svovaf)
11159 * @since 1.0.2
11160 *
11161 * @return number
11162 */
11163 function get_plan_id() {
11164 return $this->_site->plan_id;
11165 }
11166
11167 /**
11168 * Get site's plan title.
11169 *
11170 * @author Vova Feldman (@svovaf)
11171 * @since 1.0.2
11172 *
11173 * @return string
11174 */
11175 function get_plan_title() {
11176 $plan = $this->get_plan();
11177
11178 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11179 }
11180
11181 /**
11182 * Get site's plan name.
11183 *
11184 * @author Vova Feldman (@svovaf)
11185 * @since 2.0.0
11186 *
11187 * @return string
11188 */
11189 function get_plan_name() {
11190 $plan = $this->get_plan();
11191
11192 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11193 }
11194
11195 /**
11196 * @author Vova Feldman (@svovaf)
11197 * @since 1.0.9
11198 *
11199 * @return FS_Plugin_Plan|false
11200 */
11201 function get_plan() {
11202 if ( ! is_object( $this->_site ) ) {
11203 return false;
11204 }
11205
11206 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11207 $this->_get_plan_by_id( $this->_site->plan_id ) :
11208 false;
11209 }
11210
11211 /**
11212 * @author Vova Feldman (@svovaf)
11213 * @since 1.0.3
11214 *
11215 * @return bool
11216 */
11217 function is_trial() {
11218 $this->_logger->entrance();
11219
11220 if ( ! $this->is_registered() || ! is_object( $this->_site ) ) {
11221 return false;
11222 }
11223
11224 return $this->_site->is_trial();
11225 }
11226
11227 /**
11228 * Check if currently in a trial with payment method (credit card or paypal).
11229 *
11230 * @author Vova Feldman (@svovaf)
11231 * @since 1.1.7
11232 *
11233 * @return bool
11234 */
11235 function is_paid_trial() {
11236 $this->_logger->entrance();
11237
11238 if ( ! $this->is_trial() ) {
11239 return false;
11240 }
11241
11242 if ( ! $this->has_active_valid_license() ) {
11243 return false;
11244 }
11245
11246 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11247 return false;
11248 }
11249
11250 /**
11251 * @var FS_Subscription $subscription
11252 */
11253 $subscription = $this->_get_subscription( $this->_license->id );
11254
11255 return ( is_object( $subscription ) && $subscription->is_active() );
11256 }
11257
11258 /**
11259 * Check if trial already utilized.
11260 *
11261 * @since 1.0.9
11262 *
11263 * @return bool
11264 */
11265 function is_trial_utilized() {
11266 $this->_logger->entrance();
11267
11268 if ( ! $this->is_registered() ) {
11269 return false;
11270 }
11271
11272 return $this->_site->is_trial_utilized();
11273 }
11274
11275 /**
11276 * Get trial plan information (if in trial).
11277 *
11278 * @author Vova Feldman (@svovaf)
11279 * @since 1.0.9
11280 *
11281 * @return bool|FS_Plugin_Plan
11282 */
11283 function get_trial_plan() {
11284 $this->_logger->entrance();
11285
11286 if ( ! $this->is_trial() ) {
11287 return false;
11288 }
11289
11290 // Try to load plan from local cache.
11291 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11292
11293 if ( ! is_object( $trial_plan ) ) {
11294 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11295
11296 /**
11297 * If managed to fetch the plan, add it to the plans collection.
11298 */
11299 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11300 if ( ! is_array( $this->_plans ) ) {
11301 $this->_plans = array();
11302 }
11303
11304 $this->_plans[] = $trial_plan;
11305 $this->_store_plans();
11306 }
11307 }
11308
11309 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11310 return $trial_plan;
11311 }
11312
11313 /**
11314 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11315 */
11316 $trial_plan = new FS_Plugin_Plan();
11317 $trial_plan->id = $this->_site->trial_plan_id;
11318 $trial_plan->name = 'pro';
11319 $trial_plan->title = 'Pro';
11320
11321 return $trial_plan;
11322 }
11323
11324 /**
11325 * Check if the user has an activate, non-expired license on current plugin's install.
11326 *
11327 * @since 1.0.9
11328 *
11329 * @return bool
11330 */
11331 function is_paying() {
11332 $this->_logger->entrance();
11333
11334 if ( ! $this->is_registered() ) {
11335 return false;
11336 }
11337
11338 if ( ! $this->has_paid_plan() ) {
11339 return false;
11340 }
11341
11342 return (
11343 ! $this->is_trial() &&
11344 'free' !== $this->get_plan_name() &&
11345 $this->has_active_valid_license()
11346 );
11347 }
11348
11349 /**
11350 * @author Vova Feldman (@svovaf)
11351 * @since 1.0.4
11352 *
11353 * @return bool
11354 */
11355 function is_free_plan() {
11356 if ( ! $this->is_registered() ) {
11357 return true;
11358 }
11359
11360 if ( ! $this->has_paid_plan() ) {
11361 return true;
11362 }
11363
11364 return (
11365 'free' === $this->get_plan_name() ||
11366 ! $this->has_features_enabled_license()
11367 );
11368 }
11369
11370 /**
11371 * @author Vova Feldman (@svovaf)
11372 * @since 1.0.5
11373 *
11374 * @return bool
11375 */
11376 function _has_premium_license() {
11377 $this->_logger->entrance();
11378
11379 $premium_license = $this->_get_available_premium_license();
11380
11381 return ( false !== $premium_license );
11382 }
11383
11384 /**
11385 * Check if user has any licenses associated with the plugin (including expired or blocking).
11386 *
11387 * @author Vova Feldman (@svovaf)
11388 * @since 1.1.7.3
11389 *
11390 * @param bool $including_foreign
11391 *
11392 * @return bool
11393 */
11394 function has_any_license( $including_foreign = true ) {
11395 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11396 return false;
11397 }
11398
11399 if ( $including_foreign ) {
11400 return true;
11401 }
11402
11403 foreach ( $this->_licenses as $license ) {
11404 if ( $this->_user->id == $license->user_id ) {
11405 return true;
11406 }
11407 }
11408
11409 return false;
11410 }
11411
11412 /**
11413 * @author Vova Feldman (@svovaf)
11414 * @since 1.0.5
11415 *
11416 * @param bool|null $is_localhost
11417 *
11418 * @return FS_Plugin_License|false
11419 */
11420 function _get_available_premium_license( $is_localhost = null ) {
11421 $this->_logger->entrance();
11422
11423 $licenses = $this->get_available_premium_licenses( $is_localhost );
11424 if ( ! empty( $licenses ) ) {
11425 return $licenses[0];
11426 }
11427
11428 return false;
11429 }
11430
11431 /**
11432 * @author Vova Feldman (@svovaf)
11433 * @since 1.0.5
11434 *
11435 * @param bool|null $is_localhost
11436 *
11437 * @return FS_Plugin_License[]
11438 */
11439 function get_available_premium_licenses( $is_localhost = null ) {
11440 $this->_logger->entrance();
11441
11442 $licenses = array();
11443 if ( ! $this->has_paid_plan() ) {
11444 return $licenses;
11445 }
11446
11447 if ( is_array( $this->_licenses ) ) {
11448 foreach ( $this->_licenses as $license ) {
11449 if ( ! $license->can_activate( $is_localhost ) ) {
11450 continue;
11451 }
11452
11453 $licenses[] = $license;
11454 }
11455 }
11456
11457 return $licenses;
11458 }
11459
11460 /**
11461 * Sync local plugin plans with remote server.
11462 *
11463 * 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).
11464 *
11465 * @author Vova Feldman (@svovaf)
11466 * @since 1.0.5
11467 *
11468 * @return FS_Plugin_Plan[]|object
11469 */
11470 function _sync_plans() {
11471 $plans = $this->_fetch_plugin_plans();
11472
11473 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11474 $plans_map = array();
11475 foreach ( $plans as $plan ) {
11476 $plans_map[ $plan->id ] = true;
11477 }
11478
11479 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11480
11481 foreach ( $plans_ids_to_keep as $plan_id ) {
11482 if ( isset( $plans_map[ $plan_id ] ) ) {
11483 continue;
11484 }
11485
11486 $missing_plan = self::_get_plan_by_id( $plan_id );
11487
11488 if ( is_object( $missing_plan ) ) {
11489 $plans[] = $missing_plan;
11490 }
11491 }
11492
11493 $this->_plans = $plans;
11494 $this->_store_plans();
11495 }
11496
11497 $this->do_action( 'after_plans_sync', $plans );
11498
11499 return $this->_plans;
11500 }
11501
11502 /**
11503 * Check if specified plan exists locally. If not, fetch it and store it.
11504 *
11505 * @author Vova Feldman (@svovaf)
11506 * @since 2.0.0
11507 *
11508 * @param number $plan_id
11509 *
11510 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11511 */
11512 private function sync_plan_if_not_exist( $plan_id ) {
11513 $plan = self::_get_plan_by_id( $plan_id );
11514
11515 if ( is_object( $plan ) ) {
11516 // Plan already exists.
11517 return $plan;
11518 }
11519
11520 $plan = $this->fetch_plan_by_id( $plan_id );
11521
11522 if ( $plan instanceof FS_Plugin_Plan ) {
11523 $this->_plans[] = $plan;
11524 $this->_store_plans();
11525
11526 return $plan;
11527 }
11528
11529 return $plan;
11530 }
11531
11532 /**
11533 * Check if specified license exists locally. If not, fetch it and store it.
11534 *
11535 * @author Vova Feldman (@svovaf)
11536 * @since 2.0.0
11537 *
11538 * @param number $license_id
11539 * @param string $license_key
11540 *
11541 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11542 */
11543 private function sync_license_if_not_exist( $license_id, $license_key ) {
11544 $license = $this->_get_license_by_id( $license_id );
11545
11546 if ( is_object( $license ) ) {
11547 // License already exists.
11548 return $license;
11549 }
11550
11551 $license = $this->fetch_license_by_key( $license_id, $license_key );
11552
11553 if ( $license instanceof FS_Plugin_License ) {
11554 $this->_licenses[] = $license;
11555
11556 $this->set_license( $license );
11557
11558 $this->_store_licenses();
11559
11560 return $license;
11561 }
11562
11563 return $license;
11564 }
11565
11566 /**
11567 * Get a collection of unique plan IDs that are associated with any installs in the network.
11568 *
11569 * @author Leo Fajardo (@leorw)
11570 * @since 2.0.0
11571 *
11572 * @return number[]
11573 */
11574 private function get_plans_ids_associated_with_installs() {
11575 if ( ! is_multisite() ) {
11576 if ( ! is_object( $this->_site ) ||
11577 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11578 ) {
11579 return array();
11580 }
11581
11582 return array( $this->_site->plan_id );
11583 }
11584
11585 $plan_ids = array();
11586 $sites = self::get_sites();
11587 foreach ( $sites as $site ) {
11588 $blog_id = self::get_site_blog_id( $site );
11589 $install = $this->get_install_by_blog_id( $blog_id );
11590
11591 if ( ! is_object( $install ) ||
11592 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11593 ) {
11594 continue;
11595 }
11596
11597 $plan_ids[ $install->plan_id ] = true;
11598 }
11599
11600 return array_keys( $plan_ids );
11601 }
11602
11603 /**
11604 * Get a collection of unique license IDs that are associated with any installs in the network.
11605 *
11606 * @author Leo Fajardo (@leorw)
11607 * @since 2.0.0
11608 *
11609 * @return number[]
11610 */
11611 private function get_license_ids_associated_with_installs() {
11612 if ( ! $this->_is_network_active ) {
11613 if ( ! is_object( $this->_site ) ||
11614 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11615 ) {
11616 return array();
11617 }
11618
11619 return array( $this->_site->license_id );
11620 }
11621
11622 $license_ids = array();
11623 $sites = self::get_sites();
11624 foreach ( $sites as $site ) {
11625 $blog_id = self::get_site_blog_id( $site );
11626 $install = $this->get_install_by_blog_id( $blog_id );
11627
11628 if ( ! is_object( $install ) ||
11629 ! FS_Plugin_License::is_valid_id( $install->license_id )
11630 ) {
11631 continue;
11632 }
11633
11634 $license_ids[ $install->license_id ] = true;
11635 }
11636
11637 return array_keys( $license_ids );
11638 }
11639
11640 /**
11641 * @author Vova Feldman (@svovaf)
11642 * @since 1.0.5
11643 *
11644 * @param number $id
11645 *
11646 * @return FS_Plugin_Plan|false
11647 */
11648 function _get_plan_by_id( $id ) {
11649 $this->_logger->entrance();
11650
11651 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11652 $this->_sync_plans();
11653 }
11654
11655 foreach ( $this->_plans as $plan ) {
11656 if ( $id == $plan->id ) {
11657 return $plan;
11658 }
11659 }
11660
11661 return false;
11662 }
11663
11664 /**
11665 * @author Vova Feldman (@svovaf)
11666 * @since 1.1.8.1
11667 *
11668 * @param string $name
11669 *
11670 * @return FS_Plugin_Plan|false
11671 */
11672 private function get_plan_by_name( $name ) {
11673 $this->_logger->entrance();
11674
11675 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11676 $this->_sync_plans();
11677 }
11678
11679 foreach ( $this->_plans as $plan ) {
11680 if ( $name == $plan->name ) {
11681 return $plan;
11682 }
11683 }
11684
11685 return false;
11686 }
11687
11688 /**
11689 * Sync local licenses with remote server.
11690 *
11691 * @author Vova Feldman (@svovaf)
11692 * @since 1.0.6
11693 *
11694 * @param number|bool $site_license_id
11695 * @param number|null $blog_id
11696 *
11697 * @return FS_Plugin_License[]|object
11698 */
11699 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11700 $this->_logger->entrance();
11701
11702 $is_network_admin = fs_is_network_admin();
11703
11704 if ( $is_network_admin && is_null( $blog_id ) ) {
11705 $all_licenses = self::get_all_licenses( $this->_module_id );
11706 } else {
11707 $all_licenses = $this->get_user_licenses( $this->_user->id );
11708 }
11709
11710 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11711
11712 $all_licenses_map = array();
11713 foreach ( $all_licenses as $license ) {
11714 $all_licenses_map[ $license->id ] = true;
11715 }
11716
11717 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11718
11719 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11720 $licenses_map = array();
11721 foreach ( $licenses as $license ) {
11722 $licenses_map[ $license->id ] = true;
11723 }
11724
11725 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11726 // foreach ( $license_ids_to_keep as $license_id ) {
11727 // if ( isset( $licenses_map[ $license_id ] ) ) {
11728 // continue;
11729 // }
11730 //
11731 // $missing_license = self::_get_license_by_id( $license_id, false );
11732 // if ( is_object( $missing_license ) ) {
11733 // $licenses[] = $missing_license;
11734 // $licenses_map[ $missing_license->id ] = true;
11735 // }
11736 // }
11737
11738 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11739
11740 foreach ( $user_license_ids as $key => $license_id ) {
11741 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11742 // Remove access to licenses that no longer exist.
11743 unset( $user_license_ids[ $key ] );
11744 }
11745 }
11746
11747 if ( ! empty( $user_license_ids ) ) {
11748 foreach ( $licenses_map as $license_id => $value ) {
11749 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11750 // Associate new licenses with the user who triggered the license syncing.
11751 $user_license_ids[] = $license_id;
11752 }
11753 }
11754
11755 $user_license_ids = array_unique( $user_license_ids );
11756 } else {
11757 $user_license_ids = array_keys( $licenses_map );
11758 }
11759
11760 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11761 $user_licenses = array();
11762 foreach ( $licenses as $license ) {
11763 if ( ! in_array( $license->id, $user_license_ids ) ) {
11764 continue;
11765 }
11766
11767 $user_licenses[] = $license;
11768 }
11769
11770 $this->_licenses = $user_licenses;
11771 } else {
11772 $this->_licenses = $licenses;
11773 }
11774
11775 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11776
11777 $this->_store_licenses( true, $this->_module_id, $licenses );
11778 }
11779
11780 // Update current license.
11781 if ( is_object( $this->_license ) ) {
11782 $license = $this->_get_license_by_id( $this->_license->id );
11783
11784 if ( is_object( $license ) ) {
11785 /**
11786 * `$license` can be `false` in case a user change action has just been completed and this method
11787 * has synced the `$this->_licenses` collection for the new user. In this case, the
11788 * `$this->_licenses` collection may have only the newly activated license that is associated with
11789 * the new user. `set_license` will eventually be called in the same request by the logic that
11790 * follows outside this method which will detect that the install's license has been updated, and
11791 * then `_update_site_license` will be called which in turn will call `set_license`.
11792 *
11793 * @author Leo Fajardo (@leorw)
11794 * @since 2.3.2
11795 */
11796 $this->set_license( $license );
11797 }
11798 }
11799
11800 return $this->_licenses;
11801 }
11802
11803 /**
11804 * @author Vova Feldman (@svovaf)
11805 * @since 1.0.5
11806 *
11807 * @param number $id
11808 * @param bool $sync_licenses
11809 *
11810 * @return FS_Plugin_License|false
11811 */
11812 function _get_license_by_id( $id, $sync_licenses = true ) {
11813 $this->_logger->entrance();
11814
11815 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11816 return false;
11817 }
11818
11819 /**
11820 * When running from the network level admin and opted-in from the network,
11821 * check if the license exists in the network user licenses collection.
11822 *
11823 * @author Vova Feldman (@svovaf)
11824 * @since 2.0.0
11825 */
11826 if ( fs_is_network_admin() &&
11827 $this->is_network_registered() &&
11828 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11829 ) {
11830 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11831
11832 foreach ( $licenses as $license ) {
11833 if ( $id == $license->id ) {
11834 return $license;
11835 }
11836 }
11837 }
11838
11839 if ( ! $this->has_any_license() && $sync_licenses ) {
11840 $this->_sync_licenses( $id );
11841 }
11842
11843 if ( is_array( $this->_licenses ) ) {
11844 foreach ( $this->_licenses as $license ) {
11845 if ( $id == $license->id ) {
11846 return $license;
11847 }
11848 }
11849 }
11850
11851 return false;
11852 }
11853
11854 /**
11855 * 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.
11856 *
11857 * @author Vova Feldman (@svovaf)
11858 * @since 2.0.0
11859 *
11860 * @param number $id
11861 *
11862 * @return FS_Plugin_License
11863 */
11864 private function get_license_by_id( $id ) {
11865 $licenses = self::get_all_licenses( $this->_module_id );
11866
11867 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11868 foreach ( $licenses as $license ) {
11869 if ( $id == $license->id ) {
11870 return $license;
11871 }
11872 }
11873 }
11874
11875 return null;
11876 }
11877
11878 /**
11879 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11880 *
11881 * @author Vova Feldman (@svovaf)
11882 * @since 2.0.0
11883 *
11884 * @return \FS_Plugin_License|mixed
11885 */
11886 private function sync_site_license() {
11887 $api = $this->get_api_user_scope();
11888
11889 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11890
11891 if ( ! $this->is_api_result_entity( $result ) ) {
11892 return $result;
11893 }
11894
11895 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11896 $this->_store_licenses();
11897
11898 return $license;
11899 }
11900
11901 /**
11902 * Get all user's available licenses for the current module.
11903 *
11904 * @author Vova Feldman (@svovaf)
11905 * @since 2.0.0
11906 *
11907 * @param number $user_id
11908 *
11909 * @return FS_Plugin_License[]
11910 */
11911 private function get_user_licenses( $user_id ) {
11912 $all_licenses = self::get_all_licenses( $this->_module_id );
11913 if ( empty( $all_licenses ) ) {
11914 return array();
11915 }
11916
11917 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
11918 if ( empty( $user_license_ids ) ) {
11919 return array();
11920 }
11921
11922 $licenses = array();
11923 foreach ( $all_licenses as $license ) {
11924 if ( in_array( $license->id, $user_license_ids ) ) {
11925 $licenses[] = $license;
11926 }
11927 }
11928
11929 return $licenses;
11930 }
11931
11932 /**
11933 * Checks if the context license is network activated except on the given blog ID.
11934 *
11935 * @author Vova Feldman (@svovaf)
11936 * @since 2.0.0
11937 *
11938 * @param int $except_blog_id
11939 *
11940 * @return bool
11941 */
11942 private function is_license_network_active( $except_blog_id = 0 ) {
11943 $this->_logger->entrance();
11944
11945 if ( ! is_object( $this->_license ) ) {
11946 return false;
11947 }
11948
11949 $sites = self::get_sites();
11950
11951 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
11952 // There are more sites than the number of activations, so license cannot be network activated.
11953 return false;
11954 }
11955
11956 foreach ( $sites as $site ) {
11957 $blog_id = self::get_site_blog_id( $site );
11958
11959 if ( $except_blog_id == $blog_id ) {
11960 // Skip excluded blog.
11961 continue;
11962 }
11963
11964 $install = $this->get_install_by_blog_id( $blog_id );
11965
11966 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
11967 return false;
11968 }
11969 }
11970
11971 return true;
11972 }
11973
11974 /**
11975 * 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.
11976 *
11977 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
11978 *
11979 * @author Vova Feldman (@svovaf)
11980 * @since 2.0.0
11981 *
11982 * @param \FS_User $user
11983 * @param \FS_Plugin_License $license
11984 *
11985 * @return bool
11986 */
11987 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
11988 $this->_logger->entrance();
11989
11990 $result = $this->can_activate_license_on_network( $license );
11991
11992 if ( false === $result ) {
11993 return false;
11994 }
11995
11996 $installs_without_license = $result['installs'];
11997 if ( ! empty( $installs_without_license ) ) {
11998 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
11999 }
12000
12001 $disconnected_site_ids = $result['sites'];
12002 if ( ! empty( $disconnected_site_ids ) ) {
12003 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12004 }
12005
12006 $this->link_license_2_user( $license->id, $user->id );
12007
12008 // Sync license after activations.
12009 $license->activated += $result['production_count'];
12010 $license->activated_local += $result['localhost_count'];
12011
12012 // $this->_store_licenses()
12013
12014 return true;
12015 }
12016
12017 /**
12018 * Checks if the given license can be activated on the whole network.
12019 *
12020 * @author Vova Feldman (@svovaf)
12021 * @since 2.0.0
12022 *
12023 * @param \FS_Plugin_License $license
12024 *
12025 * @return false|array {
12026 * @type array[int]FS_Site $installs Blog ID to install map.
12027 * @type int[] $sites Non-connected blog IDs.
12028 * @type int $production_count Production sites count.
12029 * @type int $localhost_count Production sites count.
12030 * }
12031 */
12032 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12033 $sites = self::get_sites();
12034
12035 $production_count = 0;
12036 $localhost_count = 0;
12037
12038 $installs_without_license = array();
12039 $disconnected_site_ids = array();
12040
12041 foreach ( $sites as $site ) {
12042 $blog_id = self::get_site_blog_id( $site );
12043 $install = $this->get_install_by_blog_id( $blog_id );
12044
12045 if ( is_object( $install ) ) {
12046 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12047 // License already activated on the install.
12048 continue;
12049 }
12050
12051 $url = $install->url;
12052
12053 $installs_without_license[ $blog_id ] = $install;
12054 } else {
12055 $url = is_object( $site ) ?
12056 $site->siteurl :
12057 get_site_url( $blog_id );
12058
12059 $disconnected_site_ids[] = $blog_id;
12060 }
12061
12062 if ( FS_Site::is_localhost_by_address( $url ) ) {
12063 $localhost_count ++;
12064 } else {
12065 $production_count ++;
12066 }
12067 }
12068
12069 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12070 return false;
12071 }
12072
12073 return array(
12074 'installs' => $installs_without_license,
12075 'sites' => $disconnected_site_ids,
12076 'production_count' => $production_count,
12077 'localhost_count' => $localhost_count,
12078 );
12079 }
12080
12081 /**
12082 * Activate a given license on a collection of installs.
12083 *
12084 * @author Vova Feldman (@svovaf)
12085 * @since 2.0.0
12086 *
12087 * @param \FS_User $user
12088 * @param string $license_key
12089 * @param array $blog_2_install_map {
12090 * @key int Blog ID.
12091 * @value FS_Site Blog's associated install.
12092 * }
12093 *
12094 * @return mixed|true
12095 */
12096 private function activate_license_on_many_installs(
12097 FS_User $user,
12098 $license_key,
12099 array $blog_2_install_map
12100 ) {
12101 $params = array(
12102 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12103 );
12104
12105 $install_2_blog_map = array();
12106 foreach ( $blog_2_install_map as $blog_id => $install ) {
12107 $params[] = array( 'id' => $install->id );
12108
12109 $install_2_blog_map[ $install->id ] = $blog_id;
12110 }
12111
12112 $result = $this->get_api_user_scope_by_user( $user )->call(
12113 "plugins/{$this->_plugin->id}/installs.json",
12114 'PUT',
12115 $params
12116 );
12117
12118 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12119 return $result;
12120 }
12121
12122 foreach ( $result->installs as $r_install ) {
12123 $install = new FS_Site( $r_install );
12124 $install->is_disconnected = false;
12125
12126 // Update install.
12127 $this->_store_site(
12128 true,
12129 $install_2_blog_map[ $r_install->id ],
12130 $install
12131 );
12132 }
12133
12134 return true;
12135 }
12136
12137 /**
12138 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12139 *
12140 * @author Vova Feldman (@svovaf)
12141 * @since 2.3.1
12142 *
12143 * @param \FS_User $user
12144 * @param string $license_key
12145 *
12146 * @return true|mixed True if successful, otherwise, the API result.
12147 */
12148 private function activate_license_on_site( FS_User $user, $license_key ) {
12149 return $this->activate_license_on_many_sites( $user, $license_key );
12150 }
12151
12152 /**
12153 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12154 *
12155 * @author Vova Feldman (@svovaf)
12156 * @since 2.0.0
12157 *
12158 * @param \FS_User $user
12159 * @param string $license_key
12160 * @param int[] $site_ids
12161 *
12162 * @return true|mixed True if successful, otherwise, the API result.
12163 */
12164 private function activate_license_on_many_sites(
12165 FS_User $user,
12166 $license_key,
12167 array $site_ids = array()
12168 ) {
12169 $sites = array();
12170 foreach ( $site_ids as $site_id ) {
12171 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12172 }
12173
12174 // Install the plugin.
12175 $result = $this->create_installs_with_user(
12176 $user,
12177 $license_key,
12178 false,
12179 $sites,
12180 false,
12181 true
12182 );
12183
12184 if ( ! $this->is_api_result_entity( $result ) &&
12185 ! $this->is_api_result_object( $result, 'installs' )
12186 ) {
12187 return $result;
12188 }
12189
12190 $installs = array();
12191
12192 if ( $this->is_api_result_entity( $result ) ) {
12193 $install = new FS_Site( $result );
12194
12195 $this->_user = $user;
12196
12197 $this->_store_site( true, null, $install );
12198
12199 $this->_site = $install;
12200
12201 $this->reset_anonymous_mode();
12202 } else {
12203 foreach ( $result->installs as $install ) {
12204 $installs[] = new FS_Site( $install );
12205 }
12206
12207 // Map site addresses to their blog IDs.
12208 $address_to_blog_map = $this->get_address_to_blog_map();
12209
12210 $first_blog_id = null;
12211
12212 foreach ( $installs as $install ) {
12213 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12214 $blog_id = $address_to_blog_map[ $address ];
12215
12216 $this->_store_site( true, $blog_id, $install );
12217
12218 $this->reset_anonymous_mode( $blog_id );
12219
12220 if ( is_null( $first_blog_id ) ) {
12221 $first_blog_id = $blog_id;
12222 }
12223 }
12224
12225 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12226 $this->_storage->network_install_blog_id = $first_blog_id;
12227 }
12228 }
12229
12230 return true;
12231 }
12232
12233 /**
12234 * Sync site's license with user licenses.
12235 *
12236 * @author Vova Feldman (@svovaf)
12237 * @since 1.0.6
12238 *
12239 * @param FS_Plugin_License|null $new_license
12240 *
12241 * @return FS_Plugin_License|null
12242 */
12243 function _update_site_license( $new_license ) {
12244 $this->_logger->entrance();
12245
12246 /**
12247 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12248 * accordingly so that it will also handle the case when an ownership change is done via license
12249 * activation.
12250 *
12251 * @author Leo Fajardo (@leorw)
12252 * @since 2.3.2
12253 */
12254 $this->set_license( $new_license );
12255
12256 if ( ! is_object( $new_license ) ) {
12257 $this->_site->license_id = null;
12258 $this->_sync_site_subscription( null );
12259
12260 return $this->_license;
12261 }
12262
12263 $this->_site->license_id = $this->_license->id;
12264
12265 if ( ! is_array( $this->_licenses ) ) {
12266 $this->_licenses = array();
12267 }
12268
12269 $is_license_found = false;
12270 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12271 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12272 $this->_licenses[ $i ] = $new_license;
12273
12274 $is_license_found = true;
12275 break;
12276 }
12277 }
12278
12279 // If new license just append.
12280 if ( ! $is_license_found ) {
12281 $this->_licenses[] = $new_license;
12282 }
12283
12284 $this->_sync_site_subscription( $new_license );
12285
12286 return $this->_license;
12287 }
12288
12289 /**
12290 * @author Vova Feldman (@svovaf)
12291 * @since 2.3.1
12292 *
12293 * @param \FS_Plugin_License $license
12294 */
12295 private function set_license( FS_Plugin_License $license = null ) {
12296 $this->_license = $license;
12297
12298 $this->maybe_update_whitelabel_flag( $license );
12299 }
12300
12301 /**
12302 * @author Leo Fajardo (@leorw)
12303 * @since 2.3.1
12304 *
12305 * @param FS_Plugin_License $license
12306 */
12307 private function maybe_update_whitelabel_flag( $license ) {
12308 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12309 $this->_storage->is_whitelabeled :
12310 false;
12311
12312 if ( is_object( $license ) ) {
12313 $license_user = self::_get_user_by_id( $license->user_id );
12314
12315 if ( ! is_object( $license_user ) ) {
12316 // If foreign license, do not update the `is_whitelabeled` flag.
12317 return;
12318 }
12319
12320 if ( $this->is_addon() ) {
12321 /**
12322 * Store the last license data to the parent's storage since it's needed only when showing the
12323 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12324 * iterate over the add-ons just to get the last license data.
12325 */
12326 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12327 } else {
12328 $this->store_last_activated_license_data( $license );
12329 }
12330
12331 if ( $license->is_whitelabeled ) {
12332 // Activated a developer license, data should be hidden.
12333 $is_whitelabeled = true;
12334 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12335 // The account owner activated a regular license key, no need to hide the data.
12336 $is_whitelabeled = false;
12337 }
12338 }
12339
12340 $this->_storage->is_whitelabeled = $is_whitelabeled;
12341
12342 // Reset the whitelabeled status after update.
12343 $this->is_whitelabeled = null;
12344 if ( $this->is_addon() ) {
12345 $parent_fs = $this->get_parent_instance();
12346
12347 if ( is_object( $parent_fs ) ) {
12348 $parent_fs->is_whitelabeled = null;
12349 }
12350 }
12351 }
12352
12353 /**
12354 * @author Leo Fajardo (@leorw)
12355 * @since 2.3.1
12356 *
12357 * @param FS_Plugin_License $license
12358 * @param FS_User $license_user
12359 */
12360 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12361 if ( ! is_object( $license_user ) ) {
12362 $this->_storage->last_license_key = md5( $license->secret_key );
12363 $this->_storage->last_license_user_id = null;
12364 } else {
12365 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12366 $this->_storage->last_license_user_id = $license_user->id;
12367 }
12368 }
12369
12370 /**
12371 * @author Leo Fajardo (@leorw)
12372 * @since 2.3.1
12373 *
12374 * @param bool $ignore_data_debug_mode
12375 *
12376 * @return bool
12377 */
12378 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12379 if ( true !== $this->_storage->is_whitelabeled ) {
12380 return false;
12381 } else if ( $ignore_data_debug_mode ) {
12382 return true;
12383 }
12384
12385 $fs = $this->is_addon() ?
12386 $this->get_parent_instance() :
12387 $this;
12388
12389 return ! $fs->is_data_debug_mode();
12390 }
12391
12392 /**
12393 * @author Leo Fajardo (@leorw)
12394 * @since 2.3.1
12395 *
12396 * @return number
12397 */
12398 function get_last_license_user_id() {
12399 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12400 $this->_storage->last_license_user_id :
12401 null;
12402 }
12403
12404 /**
12405 * @author Leo Fajardo (@leorw)
12406 * @since 2.3.1
12407 *
12408 * @param int $blog_id
12409 * @param bool $ignore_data_debug_mode
12410 *
12411 * @return bool
12412 */
12413 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12414 if ( ! is_null( $blog_id ) ) {
12415 $this->switch_to_blog( $blog_id );
12416 }
12417
12418 if ( ! is_null( $this->is_whitelabeled ) ) {
12419 $is_whitelabeled = $this->is_whitelabeled;
12420 } else {
12421 $is_whitelabeled = false;
12422
12423 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12424
12425 if ( ! $this->has_addons() ) {
12426 $is_whitelabeled = $is_whitelabeled_flag;
12427 } else if ( $is_whitelabeled_flag ) {
12428 $is_whitelabeled = true;
12429 } else {
12430 $addon_ids = $this->get_updated_account_addons();
12431 $installed_addons = $this->get_installed_addons();
12432 foreach ( $installed_addons as $fs_addon ) {
12433 $addon_ids[] = $fs_addon->get_id();
12434 }
12435
12436 if ( ! empty( $addon_ids ) ) {
12437 $addon_ids = array_unique( $addon_ids );
12438
12439 $is_network_level = (
12440 fs_is_network_admin() &&
12441 $this->is_network_active()
12442 );
12443
12444 foreach ( $addon_ids as $addon_id ) {
12445 $addon = $this->get_addon( $addon_id );
12446
12447 if ( ! is_object( $addon ) ) {
12448 continue;
12449 }
12450
12451 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12452 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12453 self::get_addon_instance( $addon_id ) :
12454 null;
12455
12456 $was_addon_network_activated = false;
12457
12458 if ( is_object( $fs_addon ) ) {
12459 $was_addon_network_activated = $fs_addon->is_network_active();
12460 } else if ( $is_network_level ) {
12461 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12462 }
12463
12464 $network_delegated_connection = (
12465 $was_addon_network_activated &&
12466 $addon_storage->get( 'is_delegated_connection', false, true )
12467 );
12468
12469 if (
12470 $is_network_level &&
12471 ( ! $was_addon_network_activated || $network_delegated_connection )
12472 ) {
12473 $sites = self::get_sites();
12474
12475 /**
12476 * If in network admin area and the add-on was not network-activated or network-activated
12477 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12478 */
12479 foreach ( $sites as $site ) {
12480 $site_info = $this->get_site_info( $site );
12481
12482 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12483 $is_whitelabeled = true;
12484 break;
12485 }
12486 }
12487
12488 if ( $is_whitelabeled ) {
12489 break;
12490 }
12491 } else {
12492 /**
12493 * This will be executed when any of the following is met:
12494 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12495 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12496 * 3. Add-on was not network-activated and in site admin area.
12497 */
12498 if ( true === $addon_storage->is_whitelabeled ) {
12499 $is_whitelabeled = true;
12500 break;
12501 }
12502 }
12503 }
12504 }
12505 }
12506
12507 $this->is_whitelabeled = $is_whitelabeled;
12508
12509 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12510 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12511 }
12512
12513 if ( ! is_null( $blog_id ) ) {
12514 $this->restore_current_blog();
12515 }
12516 }
12517
12518 return (
12519 $is_whitelabeled &&
12520 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12521 );
12522 }
12523
12524 /**
12525 * Sync site's subscription.
12526 *
12527 * @author Vova Feldman (@svovaf)
12528 * @since 1.0.9
12529 *
12530 * @param FS_Plugin_License|null $license
12531 *
12532 * @return bool|\FS_Subscription
12533 */
12534 private function _sync_site_subscription( $license ) {
12535 if ( ! is_object( $license ) ) {
12536 $this->delete_unused_subscriptions();
12537
12538 return false;
12539 }
12540
12541 // Load subscription details if not lifetime.
12542 $subscription = $license->is_lifetime() ?
12543 false :
12544 $this->_fetch_site_license_subscription();
12545
12546 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12547 $this->store_subscription( $subscription );
12548 } else {
12549 $this->delete_unused_subscriptions();
12550 }
12551
12552 return $subscription;
12553 }
12554
12555 /**
12556 * @author Vova Feldman (@svovaf)
12557 * @since 1.0.6
12558 *
12559 * @return bool|\FS_Plugin_License
12560 */
12561 function _get_license() {
12562 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12563 return $this->_license;
12564 }
12565
12566 return $this->_get_available_premium_license();
12567 }
12568
12569 /**
12570 * @param number $license_id
12571 *
12572 * @return null|\FS_Subscription
12573 */
12574 function _get_subscription( $license_id ) {
12575 if ( ! isset( $this->_storage->subscriptions ) ||
12576 empty( $this->_storage->subscriptions )
12577 ) {
12578 return null;
12579 }
12580
12581 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12582 if ( $subscription->license_id == $license_id ) {
12583 return $subscription;
12584 }
12585 }
12586
12587 return null;
12588 }
12589
12590 /**
12591 * @author Leo Fajardo (@leorw)
12592 * @since 2.0.0
12593 *
12594 * @param FS_Subscription $subscription
12595 */
12596 function store_subscription( FS_Subscription $subscription ) {
12597 if ( ! isset( $this->_storage->subscriptions ) ) {
12598 $this->_storage->subscriptions = array();
12599 }
12600
12601 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12602 $this->_storage->subscriptions = array( $subscription );
12603
12604 return;
12605 }
12606
12607 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12608
12609 $updated_subscription = false;
12610 foreach ( $subscriptions as $key => $existing_subscription ) {
12611 if ( $existing_subscription->id == $subscription->id ) {
12612 $subscriptions[ $key ] = $subscription;
12613 $updated_subscription = true;
12614 break;
12615 }
12616 }
12617
12618 if ( ! $updated_subscription ) {
12619 $subscriptions[] = $subscription;
12620 }
12621
12622 $this->_storage->subscriptions = $subscriptions;
12623 }
12624
12625 /**
12626 * @author Leo Fajardo (@leorw)
12627 * @since 2.0.0
12628 */
12629 function delete_unused_subscriptions() {
12630 if ( ! isset( $this->_storage->subscriptions ) ||
12631 empty( $this->_storage->subscriptions ) ||
12632 // Clean up only if there are already at least 3 subscriptions.
12633 ( count( $this->_storage->subscriptions ) < 3 )
12634 ) {
12635 return;
12636 }
12637
12638 if ( ! is_multisite() ) {
12639 // If not multisite, there should only be 1 subscription, so just clear the array.
12640 $this->_storage->subscriptions = array();
12641
12642 return;
12643 }
12644
12645 $subscriptions_to_keep_by_license_id_map = array();
12646 $sites = self::get_sites();
12647 foreach ( $sites as $site ) {
12648 $blog_id = self::get_site_blog_id( $site );
12649 $install = $this->get_install_by_blog_id( $blog_id );
12650
12651 if ( ! is_object( $install ) ||
12652 ! FS_Plugin_License::is_valid_id( $install->license_id )
12653 ) {
12654 continue;
12655 }
12656
12657 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12658 }
12659
12660 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12661 $this->_storage->subscriptions = array();
12662
12663 return;
12664 }
12665
12666 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12667 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12668 unset( $this->_storage->subscriptions[ $key ] );
12669 }
12670 }
12671 }
12672
12673 /**
12674 * @author Vova Feldman (@svovaf)
12675 * @since 1.0.2
12676 *
12677 * @param string $plan Plan name
12678 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12679 *
12680 * @return bool
12681 */
12682 function is_plan( $plan, $exact = false ) {
12683 $this->_logger->entrance();
12684
12685 if ( ! $this->is_registered() ) {
12686 return false;
12687 }
12688
12689 $plan = strtolower( $plan );
12690
12691 $current_plan_name = $this->get_plan_name();
12692
12693 if ( $current_plan_name === $plan ) {
12694 // Exact plan.
12695 return true;
12696 } else if ( $exact ) {
12697 // Required exact, but plans are different.
12698 return false;
12699 }
12700
12701 $current_plan_order = - 1;
12702 $required_plan_order = PHP_INT_MAX;
12703 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12704 if ( $plan === $this->_plans[ $i ]->name ) {
12705 $required_plan_order = $i;
12706 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12707 $current_plan_order = $i;
12708 }
12709 }
12710
12711 return ( $current_plan_order > $required_plan_order );
12712 }
12713
12714 /**
12715 * Check if module has only one plan.
12716 *
12717 * @author Vova Feldman (@svovaf)
12718 * @since 1.2.1.7
12719 *
12720 * @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.
12721 *
12722 * @return bool
12723 */
12724 function is_single_plan( $double_check = false ) {
12725 $this->_logger->entrance();
12726
12727 if ( ! $this->is_registered() ||
12728 ! is_array( $this->_plans ) ||
12729 0 === count( $this->_plans )
12730 ) {
12731 return true;
12732 }
12733
12734 $has_free_plan = $this->has_free_plan();
12735
12736 if ( ! $has_free_plan && $double_check ) {
12737 foreach ( $this->_plans as $plan ) {
12738 if ( $plan->is_free() ) {
12739 $has_free_plan = true;
12740 break;
12741 }
12742 }
12743 }
12744
12745 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12746 }
12747
12748 /**
12749 * Check if plan based on trial. If not in trial mode, should return false.
12750 *
12751 * @since 1.0.9
12752 *
12753 * @param string $plan Plan name
12754 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12755 *
12756 * @return bool
12757 */
12758 function is_trial_plan( $plan, $exact = false ) {
12759 $this->_logger->entrance();
12760
12761 if ( ! $this->is_registered() ) {
12762 return false;
12763 }
12764
12765 if ( ! $this->is_trial() ) {
12766 return false;
12767 }
12768
12769 $trial_plan = $this->get_trial_plan();
12770
12771 if ( $trial_plan->name === $plan ) {
12772 // Exact plan.
12773 return true;
12774 } else if ( $exact ) {
12775 // Required exact, but plans are different.
12776 return false;
12777 }
12778
12779 $current_plan_order = - 1;
12780 $required_plan_order = - 1;
12781 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12782 if ( $plan === $this->_plans[ $i ]->name ) {
12783 $required_plan_order = $i;
12784 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12785 $current_plan_order = $i;
12786 }
12787 }
12788
12789 return ( $current_plan_order > $required_plan_order );
12790 }
12791
12792 /**
12793 * Check if plugin has any paid plans.
12794 *
12795 * @author Vova Feldman (@svovaf)
12796 * @since 1.0.7
12797 *
12798 * @return bool
12799 */
12800 function has_paid_plan() {
12801 return $this->_has_paid_plans ||
12802 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12803 }
12804
12805 /**
12806 * Check if plugin has any plan with a trail.
12807 *
12808 * @author Vova Feldman (@svovaf)
12809 * @since 1.0.9
12810 *
12811 * @return bool
12812 */
12813 function has_trial_plan() {
12814 /**
12815 * @author Vova Feldman(@svovaf)
12816 * @since 1.2.1.5
12817 *
12818 * Allow setting a trial from the SDK without calling the API.
12819 * But, if the user did opt-in, continue using the real data from the API.
12820 */
12821 if ( $this->_trial_days >= 0 ) {
12822 return true;
12823 }
12824
12825 return $this->_storage->get( 'has_trial_plan', false );
12826 }
12827
12828 /**
12829 * Check if plugin has any free plan, or is it premium only.
12830 *
12831 * Note: If no plans configured, assume plugin is free.
12832 *
12833 * @author Vova Feldman (@svovaf)
12834 * @since 1.0.7
12835 *
12836 * @return bool
12837 */
12838 function has_free_plan() {
12839 return ! $this->is_only_premium();
12840 }
12841
12842 /**
12843 * Displays a license activation dialog box when the user clicks on the "Activate License"
12844 * or "Change License" link on the plugins
12845 * page.
12846 *
12847 * @author Leo Fajardo (@leorw)
12848 * @since 1.1.9
12849 */
12850 function _add_license_activation_dialog_box() {
12851 $vars = array(
12852 'id' => $this->_module_id,
12853 );
12854
12855 fs_require_template( 'forms/license-activation.php', $vars );
12856 fs_require_template( 'forms/resend-key.php', $vars );
12857 }
12858
12859 /**
12860 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
12861 *
12862 * @author Leo Fajardo (@leorw)
12863 * @since 2.3.2
12864 *
12865 * @return number[]
12866 */
12867 function get_installs_ids_with_foreign_licenses() {
12868 $installs = array();
12869
12870 if (
12871 is_object( $this->_license ) &&
12872 $this->_site->user_id != $this->_license->user_id
12873 ) {
12874 $installs[] = $this->_site->id;
12875 }
12876
12877 /**
12878 * Also try to get foreign licenses for the context product's add-ons.
12879 */
12880 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
12881
12882 foreach ( $installs_by_slug_map as $slug => $install_info ) {
12883 if ( $slug == $this->get_slug() ) {
12884 continue;
12885 }
12886
12887 $install = $install_info['install'];
12888 $license = $install_info['license'];
12889
12890 if (
12891 is_object( $license ) &&
12892 $install->user_id != $license->user_id
12893 ) {
12894 $installs[] = $install->id;
12895 }
12896 }
12897
12898 return $installs;
12899 }
12900
12901 /**
12902 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
12903 *
12904 * @author Leo Fajardo (@leorw)
12905 * @since 2.3.2
12906 *
12907 * @param number[] $install_ids
12908 */
12909 function _add_user_change_dialog_box( $install_ids ) {
12910 $vars = array(
12911 'id' => $this->_module_id,
12912 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
12913 );
12914
12915 fs_require_template( 'forms/user-change.php', $vars );
12916 }
12917
12918 /**
12919 * @author Leo Fajardo (@leorw)
12920 * @since 2.3.1
12921 */
12922 function _add_data_debug_mode_dialog_box() {
12923 $vars = array(
12924 'id' => $this->_module_id,
12925 );
12926
12927 fs_require_template( 'forms/data-debug-mode.php', $vars );
12928 }
12929
12930 /**
12931 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
12932 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
12933 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
12934 * is only activated on a single production site.
12935 *
12936 * @author Leo Fajardo (@leorw)
12937 * @since 2.2.1
12938 *
12939 * @param bool $is_license_deactivation
12940 *
12941 * @return array
12942 */
12943 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
12944 if ( fs_is_network_admin() ) {
12945 // Subscription cancellation dialog box is currently not supported for multisite networks.
12946 return array();
12947 }
12948
12949 if ( $this->is_whitelabeled() ) {
12950 return array();
12951 }
12952
12953 $license = $this->_get_license();
12954
12955 /**
12956 * 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.
12957 *
12958 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
12959 * @since 2.2.1
12960 */
12961 if ( ! is_object( $license ) ||
12962 $license->is_lifetime() ||
12963 ( ! $license->is_single_site() && $license->activated > 1 )
12964 ) {
12965 return array();
12966 }
12967
12968 /**
12969 * @var FS_Subscription $subscription
12970 */
12971 $subscription = $this->_get_subscription( $license->id );
12972 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
12973 return array();
12974 }
12975
12976 return array(
12977 'id' => $this->_module_id,
12978 'license' => $license,
12979 'has_trial' => $this->is_paid_trial(),
12980 'is_license_deactivation' => $is_license_deactivation,
12981 );
12982 }
12983
12984 /**
12985 * @author Leo Fajardo (@leorw)
12986 * @since 2.0.2
12987 */
12988 function _add_premium_version_upgrade_selection_dialog_box() {
12989 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
12990 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
12991 return;
12992 }
12993
12994 $vars = array(
12995 'id' => $this->_module_id,
12996 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
12997 $modules_update->response[ $this->_plugin_basename ]->new_version :
12998 $modules_update->response[ $this->_plugin_basename ]['new_version']
12999 );
13000
13001 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13002 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13003 }
13004
13005 /**
13006 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13007 * page.
13008 *
13009 * @author Leo Fajardo (@leorw)
13010 * @since 1.2.1.5
13011 */
13012 function _add_optout_dialog() {
13013 if ( $this->is_theme() ) {
13014 $vars = null;
13015 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13016 }
13017
13018 $vars = array( 'id' => $this->_module_id );
13019 fs_require_template( 'forms/optout.php', $vars );
13020 }
13021
13022 /**
13023 * Prepare page to include all required UI and logic for the license activation dialog.
13024 *
13025 * @author Vova Feldman (@svovaf)
13026 * @since 1.2.0
13027 */
13028 function _add_license_activation() {
13029 if ( $this->is_migration() ) {
13030 return;
13031 }
13032
13033 if ( ! $this->is_user_admin() ) {
13034 // Only admins can activate a license.
13035 return;
13036 }
13037
13038 if ( ! $this->has_paid_plan() ) {
13039 // Module doesn't have any paid plans.
13040 return;
13041 }
13042
13043 if (
13044 $this->has_premium_version() &&
13045 ! $this->is_premium() &&
13046 /**
13047 * Also handle the case when an upgrade was made using the free version.
13048 *
13049 * @author Leo Fajardo (@leorw)
13050 * @since 2.3.2
13051 */
13052 ! is_object( $this->_get_license() )
13053 ) {
13054 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13055 return;
13056 }
13057
13058 // Add license activation link and AJAX request handler.
13059 if ( self::is_plugins_page() ) {
13060 $is_network_admin = fs_is_network_admin();
13061
13062 if (
13063 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13064 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13065 ) {
13066 /**
13067 * @since 1.2.0 Add license action link only on plugins page.
13068 */
13069 $this->_add_license_action_link();
13070 }
13071 }
13072
13073 // Add license activation AJAX callback.
13074 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13075
13076 // Add resend license AJAX callback.
13077 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13078 }
13079
13080 /**
13081 * Prepares page to include all required UI and logic for the "Change User" dialog.
13082 *
13083 * @author Leo Fajardo (@leorw)
13084 * @since 2.3.2
13085 */
13086 function _add_user_change_option() {
13087 if ( ! $this->should_handle_user_change() ) {
13088 return;
13089 }
13090
13091 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13092
13093 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13094 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13095 return;
13096 }
13097
13098 // Add user change AJAX handler.
13099 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13100 }
13101
13102 /**
13103 * @author Leo Fajardo (@leorw)
13104 * @since 2.3.2
13105 */
13106 function should_handle_user_change() {
13107 if ( ! $this->is_user_admin() ) {
13108 // Only admins can change user.
13109 return false;
13110 }
13111
13112 if ( $this->is_addon() ) {
13113 return false;
13114 }
13115
13116 if ( ! $this->is_registered() ) {
13117 return false;
13118 }
13119
13120 if (
13121 $this->is_network_active() &&
13122 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13123 ) {
13124 // Handle only on site-level "Account" section for now.
13125 return false;
13126 }
13127
13128 return true;
13129 }
13130
13131 /**
13132 * @author Leo Fajardo (@leorw)
13133 * @since 2.0.2
13134 */
13135 function _add_premium_version_upgrade_selection() {
13136 if ( ! $this->is_user_admin() ) {
13137 return;
13138 }
13139
13140 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13141 // This is relevant only to the free versions and premium versions without an active license.
13142 return;
13143 }
13144
13145 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13146 $this->_add_premium_version_upgrade_selection_action();
13147 }
13148 }
13149
13150 /**
13151 * @author Edgar Melkonyan
13152 * @since 2.4.1
13153 *
13154 * @throws Freemius_Exception
13155 */
13156 function _toggle_whitelabel_mode_ajax_handler() {
13157 $this->_logger->entrance();
13158
13159 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13160
13161 if ( ! $this->is_user_admin() ) {
13162 // Only for admins.
13163 self::shoot_ajax_failure();
13164 }
13165
13166 $license = $this->get_api_user_scope()->call(
13167 "/licenses/{$this->_site->license_id}.json",
13168 'put',
13169 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13170 );
13171
13172 if ( ! $this->is_api_result_entity( $license ) ) {
13173 self::shoot_ajax_failure(
13174 FS_Api::is_api_error_object( $license ) ?
13175 $license->error->message :
13176 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13177 );
13178 }
13179
13180 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13181 $this->_store_licenses();
13182
13183 $this->_sync_license();
13184
13185 if ( ! $license->is_whitelabeled ) {
13186 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13187 } else {
13188 $this->_admin_notices->add_sticky(
13189 sprintf(
13190 $this->get_text_inline(
13191 '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.',
13192 'license_whitelabeled'
13193 ),
13194 "<strong>{$this->get_plugin_title()}</strong>",
13195 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13196 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13197 ),
13198 'license_whitelabeled'
13199 );
13200 }
13201
13202 self::shoot_ajax_response( array( 'success' => true ) );
13203 }
13204
13205 /**
13206 * @author Leo Fajardo (@leorw)
13207 * @since 2.3.0
13208 */
13209 function _add_beta_mode_update_handler() {
13210 if ( ! $this->is_user_admin() ) {
13211 return;
13212 }
13213
13214 if ( ! $this->is_premium() ) {
13215 return;
13216 }
13217
13218 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13219 }
13220
13221 /**
13222 * @author Leo Fajardo (@leorw)
13223 * @since 2.3.0
13224 */
13225 function _set_beta_mode_ajax_handler() {
13226 $this->_logger->entrance();
13227
13228 $this->check_ajax_referer( 'set_beta_mode' );
13229
13230 if ( ! $this->is_user_admin() ) {
13231 // Only for admins.
13232 self::shoot_ajax_failure();
13233 }
13234
13235 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13236
13237 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13238 self::shoot_ajax_failure();
13239 }
13240
13241 $user = $this->get_api_user_scope()->call(
13242 '',
13243 'put',
13244 array(
13245 'plugin_id' => $this->get_id(),
13246 'is_beta' => ( 'true' == $is_beta ),
13247 'fields' => 'is_beta'
13248 )
13249 );
13250
13251 if ( ! $this->is_api_result_entity( $user ) ) {
13252 self::shoot_ajax_failure(
13253 FS_Api::is_api_error_object( $user ) ?
13254 $user->error->message :
13255 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13256 );
13257 }
13258
13259 $this->_user->is_beta = $user->is_beta;
13260 $this->_store_user();
13261
13262 self::shoot_ajax_response( array( 'success' => true ) );
13263 }
13264
13265 /**
13266 * License activation WP AJAX handler.
13267 *
13268 * @author Leo Fajardo (@leorw)
13269 * @since 1.1.9
13270 *
13271 * @uses Freemius::activate_license()
13272 */
13273 function _activate_license_ajax_action() {
13274 $this->_logger->entrance();
13275
13276 $this->check_ajax_referer( 'activate_license' );
13277
13278 $license_key = trim( fs_request_get( 'license_key' ) );
13279
13280 if ( empty( $license_key ) ) {
13281 exit;
13282 }
13283
13284 $sites = fs_is_network_admin() ?
13285 fs_request_get( 'sites', array(), 'post' ) :
13286 array();
13287
13288 $result = $this->activate_license(
13289 $license_key,
13290 $sites,
13291 fs_request_get_bool( 'is_marketing_allowed', null ),
13292 fs_request_get( 'blog_id', null ),
13293 fs_request_get( 'module_id', null, 'post' ),
13294 fs_request_get( 'user_id', null ),
13295 fs_request_get_bool( 'is_extensions_tracking_allowed', true )
13296 );
13297
13298 if (
13299 $result['success'] &&
13300 $this->is_bundle_license_auto_activation_enabled()
13301 ) {
13302 $license = new FS_Plugin_License();
13303 $license->secret_key = $license_key;
13304
13305 $this->maybe_activate_bundle_license( $license, $sites );
13306 }
13307
13308 echo json_encode( $result );
13309
13310 exit;
13311 }
13312
13313 /**
13314 * User change WP AJAX handler.
13315 *
13316 * @author Leo Fajardo (@leorw)
13317 * @since 2.3.2
13318 */
13319 function _user_change_ajax_action() {
13320 $this->_logger->entrance();
13321
13322 $this->check_ajax_referer( 'change_user' );
13323
13324 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13325 $new_user_id = fs_request_get( 'user_id' );
13326
13327 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13328 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13329 }
13330
13331 $params = array();
13332
13333 if ( ! empty( $new_email_address ) ) {
13334 $params['user_email'] = $new_email_address;
13335 } else {
13336 $params['user_id'] = $new_user_id;
13337 }
13338
13339 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13340 $install_ids = array();
13341
13342 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13343 $install_ids[ $slug ] = $install_info['install']->id;
13344 }
13345
13346 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13347
13348 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13349
13350 if ( FS_Api::is_api_error( $install ) ) {
13351 $error = '';
13352
13353 if ( is_object( $install ) ) {
13354 switch ( $install->error->code ) {
13355 case 'user_exist':
13356 $error = (
13357 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13358 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13359 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>' ) .
13360 sprintf(
13361 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13362 $this->get_account_url( 'change_owner', array(
13363 'state' => 'init',
13364 'candidate_email' => $new_email_address
13365 ) ),
13366 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13367 )
13368 );
13369 break;
13370 }
13371 }
13372
13373 if ( empty( $error ) ) {
13374 $error = FS_Api::is_api_error_object( $install ) ?
13375 $install->error->message :
13376 var_export( $install->error, true );
13377 }
13378
13379 self::shoot_ajax_failure( $error );
13380 } else {
13381 if (
13382 // If successful ownership change.
13383 $this->get_user()->id != $install->user_id ||
13384 ! empty( $new_email_address )
13385 ) {
13386 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13387 }
13388 }
13389
13390 self::shoot_ajax_success();
13391 }
13392
13393 /**
13394 * @author Leo Fajardo (@leorw)
13395 * @since 2.3.2.14
13396 */
13397 function starting_migration() {
13398 if ( ! empty( $this->_storage->license_migration ) ) {
13399 // Do not overwrite the data if already set.
13400 return;
13401 }
13402
13403 $this->_storage->license_migration = array(
13404 'is_migrating' => true,
13405 'start_timestamp' => time()
13406 );
13407 }
13408
13409 /**
13410 * @author Leo Fajardo (@leorw)
13411 * @since 2.3.2.14
13412 */
13413 function is_migration() {
13414 if ( $this->is_addon() ) {
13415 return $this->get_parent_instance()->is_migration();
13416 }
13417
13418 if ( empty( $this->_storage->license_migration ) ) {
13419 return false;
13420 }
13421
13422 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13423 return false;
13424 }
13425
13426 return (
13427 // Return `true` if the migration is within 5 minutes from the starting time.
13428 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13429 );
13430 }
13431
13432 /**
13433 *
13434 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13435 *
13436 * @author Vova Feldman (@svovaf)
13437 * @since 2.3.0
13438 *
13439 * @param string $license_key
13440 * @param null|bool $is_marketing_allowed
13441 * @param null|number $plugin_id
13442 * @param array $sites
13443 * @param int $blog_id
13444 *
13445 * @return array {
13446 * @var bool $success
13447 * @var string $error
13448 * @var string $next_page
13449 * }
13450 *
13451 * @uses Freemius::activate_license()
13452 */
13453 function activate_migrated_license(
13454 $license_key,
13455 $is_marketing_allowed = null,
13456 $plugin_id = null,
13457 $sites = array(),
13458 $blog_id = null
13459 ) {
13460 $this->_logger->entrance();
13461
13462 $result = $this->activate_license(
13463 $license_key,
13464 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13465 $this->get_sites_for_network_level_optin() :
13466 $sites,
13467 $is_marketing_allowed,
13468 $blog_id,
13469 $plugin_id
13470 );
13471
13472 // No need to show the sticky after license activation notice after migrating a license.
13473 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13474
13475 return $result;
13476 }
13477
13478 /**
13479 * @author Leo Fajardo (@leorw)
13480 * @since 2.3.1
13481 *
13482 * @return string
13483 */
13484 function get_pricing_js_path() {
13485 return $this->apply_filters( 'freemius_pricing_js_path', WP_FS__DIR_INCLUDES . '/freemius-pricing/freemius-pricing.js' );
13486 }
13487
13488 /**
13489 * @author Leo Fajardo (@leorw)
13490 * @since 2.3.1
13491 *
13492 * @return bool
13493 */
13494 function should_use_external_pricing() {
13495 if ( is_null( $this->_use_external_pricing ) ) {
13496 $pricing_js_path = $this->get_pricing_js_path();
13497
13498 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
13499 }
13500
13501 return $this->_use_external_pricing;
13502 }
13503
13504 /**
13505 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13506 *
13507 * @author Vova Feldman (@svovaf)
13508 * @since 2.2.4
13509 * @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).
13510 * @param string $license_key
13511 * @param array $sites
13512 * @param null|bool $is_marketing_allowed
13513 * @param null|int $blog_id
13514 * @param null|number $plugin_id
13515 * @param null|number $license_owner_id
13516 *
13517 * @return array {
13518 * @var bool $success
13519 * @var string $error
13520 * @var string $next_page
13521 * }
13522 */
13523 private function activate_license(
13524 $license_key,
13525 $sites = array(),
13526 $is_marketing_allowed = null,
13527 $blog_id = null,
13528 $plugin_id = null,
13529 $license_owner_id = null,
13530 $is_extensions_tracking_allowed = true
13531 ) {
13532 $this->_logger->entrance();
13533
13534 $license_key = trim( $license_key );
13535
13536 $is_network_activation_or_migration = (
13537 fs_is_network_admin() ||
13538 ( ! empty( $sites ) && $this->is_migration() )
13539 );
13540
13541 if ( ! $is_network_activation_or_migration ) {
13542 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13543 $sites = array();
13544 }
13545
13546 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13547 $this :
13548 $this->get_addon_instance( $plugin_id );
13549
13550 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
13551
13552 $error = false;
13553 $next_page = false;
13554
13555 $has_valid_blog_id = is_numeric( $blog_id );
13556
13557 $user = null;
13558
13559 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13560 /**
13561 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13562 *
13563 * @author Vova Feldman (@svovaf)
13564 */
13565 $user = $fs->get_parent_instance()->get_current_or_network_user();
13566 } else if ( $fs->is_registered() ) {
13567 $user = $fs->get_current_or_network_user();
13568 }
13569
13570 if ( $has_valid_blog_id ) {
13571 /**
13572 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13573 *
13574 * @author Leo Fajardo (@leorw)
13575 */
13576 $fs->switch_to_blog( $blog_id );
13577 }
13578
13579 if ( is_object( $user ) ) {
13580 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13581 // If no specific blog ID was provided, activate the license for all sites in the network.
13582 $blog_2_install_map = array();
13583 $site_ids = array();
13584
13585 foreach ( $sites as $site ) {
13586 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13587 continue;
13588 }
13589
13590 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13591
13592 if ( is_object( $install ) ) {
13593 $blog_2_install_map[ $site['blog_id'] ] = $install;
13594 } else {
13595 $site_ids[] = $site['blog_id'];
13596 }
13597 }
13598
13599 if ( ! empty( $blog_2_install_map ) ) {
13600 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13601
13602 if ( true !== $result ) {
13603 $error = FS_Api::is_api_error_object( $result ) ?
13604 $result->error->message :
13605 var_export( $result, true );
13606 }
13607 }
13608
13609 if ( empty( $error ) && ! empty( $site_ids ) ) {
13610 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13611
13612 if ( true !== $result ) {
13613 $error = FS_Api::is_api_error_object( $result ) ?
13614 $result->error->message :
13615 var_export( $result, true );
13616 }
13617 }
13618 } else {
13619 if ( $fs->is_registered() ) {
13620 $params = array(
13621 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13622 );
13623
13624 $install_ids = array();
13625
13626 $change_owner = FS_User::is_valid_id( $license_owner_id );
13627
13628 if ( $change_owner ) {
13629 $params['user_id'] = $license_owner_id;
13630
13631 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13632
13633 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13634 $install_ids[ $slug ] = $install_info['install']->id;
13635 }
13636
13637 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13638 }
13639
13640 $api = $fs->get_api_site_scope();
13641
13642 $install = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13643
13644 if ( FS_Api::is_api_error( $install ) ) {
13645 $error = FS_Api::is_api_error_object( $install ) ?
13646 $install->error->message :
13647 var_export( $install->error, true );
13648 } else {
13649 $fs->reconnect_locally( $has_valid_blog_id );
13650
13651 if (
13652 $change_owner &&
13653 // If successful ownership change.
13654 $fs->get_user()->id != $install->user_id
13655 ) {
13656 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13657 }
13658 }
13659 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13660 $result = $fs->activate_license_on_site( $user, $license_key );
13661
13662 if ( true !== $result ) {
13663 $error = FS_Api::is_api_error_object( $result ) ?
13664 $result->error->message :
13665 var_export( $result, true );
13666 }
13667 }
13668 }
13669
13670 if ( empty( $error ) ) {
13671 $fs->network_upgrade_mode_completed();
13672
13673 $fs->_user = $user;
13674
13675 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13676 $fs->_site = $fs->get_network_install();
13677 }
13678
13679 $fs->_sync_license( true, $has_valid_blog_id );
13680
13681 $this->maybe_sync_install_user();
13682
13683 $next_page = $fs->is_addon() ?
13684 $fs->get_parent_instance()->get_account_url() :
13685 $fs->get_after_activation_url( 'after_connect_url' );
13686 }
13687 } else {
13688 $next_page = $fs->opt_in(
13689 false,
13690 false,
13691 false,
13692 $license_key,
13693 false,
13694 false,
13695 false,
13696 $is_marketing_allowed,
13697 $sites
13698 );
13699
13700 if ( isset( $next_page->error ) ) {
13701 $error = $next_page->error;
13702 } else {
13703 if ( $is_network_activation_or_migration ) {
13704 /**
13705 * Get the list of sites that were just opted-in (and license activated).
13706 * This is an optimization for the next part below saving some DB queries.
13707 */
13708 $connected_sites = array();
13709 foreach ( $sites as $site ) {
13710 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13711 $connected_sites[ $site['blog_id'] ] = true;
13712 }
13713 }
13714
13715 $all_sites = self::get_sites();
13716 $pending_sites = array();
13717
13718 /**
13719 * 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.
13720 *
13721 * @author Vova Feldman (@svovaf)
13722 */
13723 foreach ( $all_sites as $site ) {
13724 $blog_id = self::get_site_blog_id( $site );
13725
13726 if ( isset( $connected_sites[ $blog_id ] ) ) {
13727 // Site was just connected.
13728 continue;
13729 }
13730
13731 if ( $fs->is_installed_on_site( $blog_id ) ) {
13732 // Site was already connected before.
13733 continue;
13734 }
13735
13736 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13737 // Site's connection was delegated.
13738 continue;
13739 }
13740
13741 if ( $fs->is_anonymous_site( $blog_id ) ) {
13742 // Site connection was already skipped.
13743 continue;
13744 }
13745
13746 $pending_sites[] = self::get_site_info( $site );
13747 }
13748
13749 if ( ! empty( $pending_sites ) ) {
13750 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13751 $fs->skip_connection( $pending_sites );
13752 } else {
13753 $fs->delegate_connection( $pending_sites );
13754 }
13755 }
13756 }
13757 }
13758 }
13759
13760 if ( false === $error && true === $fs->_storage->require_license_activation ) {
13761 $fs->_storage->require_license_activation = false;
13762 }
13763
13764 $result = array(
13765 'success' => ( false === $error )
13766 );
13767
13768 if ( false !== $error ) {
13769 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
13770 } else {
13771 if ( $fs->is_addon() || $fs->has_addons() ) {
13772 /**
13773 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
13774 * an updated valid user licenses collection will be fetched from the server which is used to also
13775 * update the account add-ons (add-ons the user has licenses for).
13776 *
13777 * @author Leo Fajardo (@leorw)
13778 * @since 2.2.4
13779 */
13780 $fs->purge_valid_user_licenses_cache();
13781 }
13782
13783 $result['next_page'] = $next_page;
13784 }
13785
13786 return $result;
13787 }
13788
13789 /**
13790 * @author Leo Fajardo (@leorw)
13791 * @since 2.3.2
13792 *
13793 * @return array {
13794 * @key string Product slug.
13795 * @value array {
13796 * @property FS_Site $site
13797 * @property FS_Plugin_License $license
13798 * }
13799 * }
13800 */
13801 private function get_parent_and_addons_installs_info() {
13802 $fs = $this->is_addon() ?
13803 $this->get_parent_instance() :
13804 $this;
13805
13806 $installed_addons_ids = array();
13807
13808 $installed_addons_instances = $fs->get_installed_addons();
13809 foreach ( $installed_addons_instances as $instance ) {
13810 $installed_addons_ids[] = $instance->get_id();
13811 }
13812
13813 $addons_ids = array_unique( array_merge(
13814 $installed_addons_ids,
13815 $fs->get_updated_account_addons()
13816 ) );
13817
13818 // Add parent product info.
13819 $installs_info_by_slug_map = array(
13820 $fs->get_slug() => array(
13821 'install' => $fs->get_site(),
13822 'license' => $fs->_get_license()
13823 )
13824 );
13825
13826 foreach ( $addons_ids as $addon_id ) {
13827 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
13828
13829 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
13830
13831 if ( ! $addon_info['is_connected'] ) {
13832 // Add-on is not associated with an install entity.
13833 continue;
13834 }
13835
13836 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
13837 'install' => $addon_info['site'],
13838 'license' => isset( $addon_info['license'] ) ?
13839 $addon_info['license'] :
13840 null
13841 );
13842 }
13843
13844 return $installs_info_by_slug_map;
13845 }
13846
13847 /**
13848 * @author Leo Fajardo (@leorw)
13849 * @since 1.2.3.1
13850 */
13851 function _network_activate_ajax_action() {
13852 $this->_logger->entrance();
13853
13854 $this->check_ajax_referer( 'network_activate' );
13855
13856 $plugin_id = fs_request_get( 'module_id', '', 'post' );
13857 $fs = ( $plugin_id == $this->_module_id ) ?
13858 $this :
13859 $this->get_addon_instance( $plugin_id );
13860
13861 $error = false;
13862
13863 $sites = fs_request_get( 'sites', array(), 'post' );
13864 if ( is_array( $sites ) && ! empty( $sites ) ) {
13865 $sites_by_action = array(
13866 'allow' => array(),
13867 'delegate' => array(),
13868 'skip' => array()
13869 );
13870
13871 foreach ( $sites as $site ) {
13872 $sites_by_action[ $site['action'] ][] = $site;
13873 }
13874
13875 $total_sites = count( $sites );
13876 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
13877
13878 $next_page = '';
13879
13880 $has_any_install = fs_request_get_bool( 'has_any_install' );
13881
13882 if ( $total_sites === $total_sites_to_delegate &&
13883 ! $this->is_network_upgrade_mode() &&
13884 ! $has_any_install
13885 ) {
13886 $this->delegate_connection();
13887 } else {
13888 if ( ! empty( $sites_by_action['delegate'] ) ) {
13889 $this->delegate_connection( $sites_by_action['delegate'] );
13890 }
13891
13892 if ( ! empty( $sites_by_action['skip'] ) ) {
13893 $this->skip_connection( $sites_by_action['skip'] );
13894 }
13895
13896 if ( empty( $sites_by_action['allow'] ) ) {
13897 if ( $has_any_install ) {
13898 $first_install = $fs->find_first_install();
13899
13900 if ( ! is_null( $first_install ) ) {
13901 $fs->_site = $first_install['install'];
13902 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
13903
13904 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
13905 $fs->_storage->network_user_id = $fs->_user->id;
13906 }
13907 }
13908 } else {
13909 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
13910 $next_page = $fs->opt_in(
13911 false,
13912 false,
13913 false,
13914 false,
13915 false,
13916 false,
13917 false,
13918 fs_request_get_bool( 'is_marketing_allowed', null ),
13919 $sites_by_action['allow']
13920 );
13921 } else {
13922 $next_page = $fs->install_with_user(
13923 $this->get_network_user(),
13924 false,
13925 false,
13926 false,
13927 true,
13928 $sites_by_action['allow']
13929 );
13930 }
13931
13932 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
13933 $error = $next_page->error;
13934 }
13935 }
13936 }
13937
13938 if ( empty( $next_page ) ) {
13939 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
13940 }
13941 } else {
13942 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
13943 }
13944
13945 $result = array(
13946 'success' => ( false === $error )
13947 );
13948
13949 if ( false !== $error ) {
13950 $result['error'] = $error;
13951 } else {
13952 $result['next_page'] = $next_page;
13953 }
13954
13955 echo json_encode( $result );
13956
13957 exit;
13958 }
13959
13960 /**
13961 * Billing update AJAX callback.
13962 *
13963 * @author Vova Feldman (@svovaf)
13964 * @since 1.2.1.5
13965 */
13966 function _update_billing_ajax_action() {
13967 $this->_logger->entrance();
13968
13969 $this->check_ajax_referer( 'update_billing' );
13970
13971 if ( ! $this->is_user_admin() ) {
13972 // Only for admins.
13973 self::shoot_ajax_failure();
13974 }
13975
13976 $billing = fs_request_get( 'billing' );
13977
13978 $api = $this->get_api_user_scope();
13979 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
13980 'plugin_id' => $this->get_parent_id(),
13981 ) ) );
13982
13983 if ( ! $this->is_api_result_entity( $result ) ) {
13984 self::shoot_ajax_failure();
13985 }
13986
13987 // Purge cached billing.
13988 $this->get_api_user_scope()->purge_cache( 'billing.json' );
13989
13990 self::shoot_ajax_success();
13991 }
13992
13993 /**
13994 * Trial start for anonymous users (AJAX callback).
13995 *
13996 * @author Vova Feldman (@svovaf)
13997 * @since 1.2.1.5
13998 */
13999 function _start_trial_ajax_action() {
14000 $this->_logger->entrance();
14001
14002 $this->check_ajax_referer( 'start_trial' );
14003
14004 if ( ! $this->is_user_admin() ) {
14005 // Only for admins.
14006 self::shoot_ajax_failure();
14007 }
14008
14009 $trial_data = fs_request_get( 'trial' );
14010
14011 $next_page = $this->opt_in(
14012 false,
14013 false,
14014 false,
14015 false,
14016 false,
14017 $trial_data['plan_id']
14018 );
14019
14020 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14021 self::shoot_ajax_failure(
14022 isset( $next_page->error ) ?
14023 $next_page->error->message :
14024 var_export( $next_page, true )
14025 );
14026 }
14027
14028 $this->shoot_ajax_success( array(
14029 'next_page' => $next_page,
14030 ) );
14031 }
14032
14033 /**
14034 * @author Leo Fajardo (@leorw)
14035 * @since 1.2.0
14036 */
14037 function _resend_license_key_ajax_action() {
14038 $this->_logger->entrance();
14039
14040 $this->check_ajax_referer( 'resend_license_key' );
14041
14042 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14043
14044 if ( empty( $email_address ) ) {
14045 exit;
14046 }
14047
14048 $error = false;
14049
14050 $api = $this->get_api_plugin_scope();
14051 $result = $api->call( '/licenses/resend.json', 'post',
14052 array(
14053 'email' => $email_address,
14054 'url' => home_url(),
14055 )
14056 );
14057
14058 if ( is_object( $result ) && isset( $result->error ) ) {
14059 $error = $result->error;
14060
14061 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14062 $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' );
14063 } else if ( 'no_license' === $error->code ) {
14064 $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' );
14065 } else {
14066 $error = $error->message;
14067 }
14068 }
14069
14070 $licenses = array(
14071 'success' => ( false === $error )
14072 );
14073
14074 if ( false !== $error ) {
14075 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14076 }
14077
14078 echo json_encode( $licenses );
14079
14080 exit;
14081 }
14082
14083 /**
14084 * @author Vova Feldman (@svovaf)
14085 * @since 1.2.1.8
14086 *
14087 * @var string
14088 */
14089 private static $_pagenow;
14090
14091 /**
14092 * Get current page or the referer if executing a WP AJAX request.
14093 *
14094 * @author Vova Feldman (@svovaf)
14095 * @since 1.2.1.8
14096 *
14097 * @return string
14098 */
14099 static function get_current_page() {
14100 if ( ! isset( self::$_pagenow ) ) {
14101 global $pagenow;
14102 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14103 /**
14104 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14105 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14106 *
14107 * @author Leo Fajardo (@leorw)
14108 * @since 2.2.3
14109 */
14110 if ( is_network_admin() ) {
14111 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14112 } else if ( is_user_admin() ) {
14113 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14114 } else {
14115 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14116 }
14117
14118 $pagenow = $self_matches[1];
14119 $pagenow = trim( $pagenow, '/' );
14120 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14121 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14122 $pagenow = 'index.php';
14123 } else {
14124 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14125 $pagenow = strtolower( $self_matches[1] );
14126 if ( '.php' !== substr($pagenow, -4, 4) )
14127 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14128 }
14129 }
14130
14131 self::$_pagenow = $pagenow;
14132
14133 if ( self::is_ajax() &&
14134 'admin-ajax.php' === $pagenow
14135 ) {
14136 $referer = fs_get_raw_referer();
14137
14138 if ( is_string( $referer ) ) {
14139 $parts = explode( '?', $referer );
14140
14141 self::$_pagenow = basename( $parts[0] );
14142 }
14143 }
14144 }
14145
14146 return self::$_pagenow;
14147 }
14148
14149 /**
14150 * Helper method to check if user in the plugins page.
14151 *
14152 * @author Vova Feldman (@svovaf)
14153 * @since 1.2.1.5
14154 *
14155 * @return bool
14156 */
14157 static function is_plugins_page() {
14158 return ( 'plugins.php' === self::get_current_page() );
14159 }
14160
14161 /**
14162 * @author Leo Fajardo (@leorw)
14163 * @since 2.2.3
14164 *
14165 * @return bool
14166 */
14167 static function is_plugin_install_page() {
14168 return ( 'plugin-install.php' === self::get_current_page() );
14169 }
14170
14171 /**
14172 * @author Leo Fajardo (@leorw)
14173 * @since 2.0.2
14174 *
14175 * @return bool
14176 */
14177 static function is_updates_page() {
14178 return ( 'update-core.php' === self::get_current_page() );
14179 }
14180
14181 /**
14182 * Helper method to check if user in the themes page.
14183 *
14184 * @author Vova Feldman (@svovaf)
14185 * @since 1.2.2.6
14186 *
14187 * @return bool
14188 */
14189 static function is_themes_page() {
14190 return ( 'themes.php' === self::get_current_page() );
14191 }
14192
14193 #----------------------------------------------------------------------------------
14194 #region Affiliation
14195 #----------------------------------------------------------------------------------
14196
14197 /**
14198 * @author Leo Fajardo (@leorw)
14199 * @since 1.2.3
14200 *
14201 * @return bool
14202 */
14203 function has_affiliate_program() {
14204 if ( ! is_object( $this->_plugin ) ) {
14205 return false;
14206 }
14207
14208 return $this->_plugin->has_affiliate_program();
14209 }
14210
14211 /**
14212 * @author Leo Fajardo (@leorw)
14213 * @since 1.2.4
14214 */
14215 private function fetch_affiliate_terms() {
14216 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14217 $plugins_api = $this->get_api_plugin_scope();
14218 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14219
14220 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14221 return;
14222 }
14223
14224 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14225 }
14226 }
14227
14228 /**
14229 * @author Leo Fajardo (@leorw)
14230 * @since 1.2.4
14231 */
14232 private function fetch_affiliate_and_custom_terms() {
14233 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14234 $application_data = $this->_storage->affiliate_application_data;
14235 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14236
14237 $users_api = $this->get_api_user_scope();
14238 $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14239 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14240 if ( ! empty( $result->affiliates ) ) {
14241 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14242
14243 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14244 $application_data['status'] = $affiliate->status;
14245 $this->_storage->affiliate_application_data = $application_data;
14246 }
14247
14248 if ( $affiliate->is_using_custom_terms ) {
14249 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14250 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14251 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14252 }
14253 }
14254
14255 $this->affiliate = $affiliate;
14256 }
14257 }
14258 }
14259 }
14260
14261 /**
14262 * @author Leo Fajardo (@leorw)
14263 * @since 1.2.3
14264 */
14265 private function fetch_affiliate_and_terms() {
14266 $this->_logger->entrance();
14267
14268 $this->fetch_affiliate_terms();
14269 $this->fetch_affiliate_and_custom_terms();
14270 }
14271
14272 /**
14273 * @author Leo Fajardo (@leorw)
14274 * @since 1.2.3
14275 *
14276 * @return FS_Affiliate
14277 */
14278 function get_affiliate() {
14279 return $this->affiliate;
14280 }
14281
14282
14283 /**
14284 * @author Leo Fajardo (@leorw)
14285 * @since 1.2.3
14286 *
14287 * @return FS_AffiliateTerms
14288 */
14289 function get_affiliate_terms() {
14290 return is_object( $this->custom_affiliate_terms ) ?
14291 $this->custom_affiliate_terms :
14292 $this->plugin_affiliate_terms;
14293 }
14294
14295 /**
14296 * @author Leo Fajardo (@leorw)
14297 * @since 1.2.3
14298 */
14299 function _submit_affiliate_application() {
14300 $this->_logger->entrance();
14301
14302 $this->check_ajax_referer( 'submit_affiliate_application' );
14303
14304 if ( ! $this->is_user_admin() ) {
14305 // Only for admins.
14306 self::shoot_ajax_failure();
14307 }
14308
14309 $affiliate = fs_request_get( 'affiliate' );
14310
14311 if ( empty( $affiliate['promotion_methods'] ) ) {
14312 unset( $affiliate['promotion_methods'] );
14313 }
14314
14315 if ( ! empty( $affiliate['additional_domains'] ) ) {
14316 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14317 }
14318
14319 if ( ! $this->is_registered() ) {
14320 // Opt in but don't track usage.
14321 $next_page = $this->opt_in(
14322 false,
14323 false,
14324 false,
14325 false,
14326 false,
14327 false,
14328 true
14329 );
14330
14331 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14332 self::shoot_ajax_failure(
14333 isset( $next_page->error ) ?
14334 $next_page->error->message :
14335 var_export( $next_page, true )
14336 );
14337 } else if ( $this->is_pending_activation() ) {
14338 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) );
14339 }
14340 }
14341
14342 $this->fetch_affiliate_terms();
14343
14344 $api = $this->get_api_user_scope();
14345 $result = $api->call(
14346 ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14347 'post',
14348 $affiliate
14349 );
14350
14351 if ( $this->is_api_error( $result ) ) {
14352 self::shoot_ajax_failure(
14353 isset( $result->error ) ?
14354 $result->error->message :
14355 var_export( $result, true )
14356 );
14357 } else {
14358 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14359 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14360 }
14361
14362 $affiliate_application_data = array(
14363 'status' => 'pending',
14364 'stats_description' => $affiliate['stats_description'],
14365 'promotion_method_description' => $affiliate['promotion_method_description'],
14366 );
14367
14368 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14369 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14370 }
14371
14372 if ( ! empty( $affiliate['domain'] ) ) {
14373 $affiliate_application_data['domain'] = $affiliate['domain'];
14374 }
14375
14376 if ( ! empty( $affiliate['additional_domains'] ) ) {
14377 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14378 }
14379
14380 $this->_storage->affiliate_application_data = $affiliate_application_data;
14381 }
14382
14383 // Purge cached affiliate.
14384 $api->purge_cache( 'affiliate.json' );
14385
14386 self::shoot_ajax_success( $result );
14387 }
14388
14389 /**
14390 * @author Leo Fajardo (@leorw)
14391 * @since 1.2.3
14392 *
14393 * @return array|null
14394 */
14395 function get_affiliate_application_data() {
14396 if ( empty( $this->_storage->affiliate_application_data ) ) {
14397 return null;
14398 }
14399
14400 return $this->_storage->affiliate_application_data;
14401 }
14402
14403 #endregion Affiliation ------------------------------------------------------------
14404
14405 #----------------------------------------------------------------------------------
14406 #region URL Generators
14407 #----------------------------------------------------------------------------------
14408
14409 /**
14410 * Alias to pricing_url().
14411 *
14412 * @author Vova Feldman (@svovaf)
14413 * @since 1.0.2
14414 *
14415 * @uses pricing_url()
14416 *
14417 * @param string $period Billing cycle
14418 * @param bool $is_trial
14419 *
14420 * @return string
14421 */
14422 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14423 return $this->pricing_url( $period, $is_trial );
14424 }
14425
14426 /**
14427 * @author Vova Feldman (@svovaf)
14428 * @since 1.0.9
14429 *
14430 * @uses get_upgrade_url()
14431 *
14432 * @return string
14433 */
14434 function get_trial_url() {
14435 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14436 }
14437
14438 /**
14439 * @author Leo Fajardo (@leorw)
14440 * @since 2.1.4
14441 *
14442 * @param string $new_version
14443 *
14444 * @return string
14445 */
14446 function version_upgrade_checkout_link( $new_version ) {
14447 if ( ! is_object( $this->_license ) ) {
14448 $url = $this->pricing_url();
14449
14450 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14451 } else {
14452 $subscription = $this->_get_subscription( $this->_license->id );
14453
14454 $url = $this->checkout_url(
14455 is_object( $subscription ) ?
14456 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14457 WP_FS__PERIOD_LIFETIME,
14458 false,
14459 array( 'licenses' => $this->_license->quota )
14460 );
14461
14462 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14463 }
14464
14465 return sprintf(
14466 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14467 sprintf( '<a href="%s">%s</a>', $url, $purchase_license_text ),
14468 $new_version
14469 );
14470 }
14471
14472 /**
14473 * Plugin's pricing URL.
14474 *
14475 * @author Vova Feldman (@svovaf)
14476 * @since 1.0.4
14477 *
14478 * @param string $billing_cycle Billing cycle
14479 *
14480 * @param bool $is_trial
14481 *
14482 * @return string
14483 */
14484 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14485 $this->_logger->entrance();
14486
14487 $params = array(
14488 'billing_cycle' => $billing_cycle
14489 );
14490
14491 if ( $is_trial ) {
14492 $params['trial'] = 'true';
14493 }
14494
14495 $url = $this->is_addon() ?
14496 $this->_parent->addon_url( $this->_slug ) :
14497 $this->_get_admin_page_url( 'pricing', $params );
14498
14499 return $this->apply_filters( 'pricing_url', $url );
14500 }
14501
14502 /**
14503 * Checkout page URL.
14504 *
14505 * @author Vova Feldman (@svovaf)
14506 * @since 1.0.6
14507 *
14508 * @param string $billing_cycle Billing cycle
14509 * @param bool $is_trial
14510 * @param array $extra (optional) Extra parameters, override other query params.
14511 * @param bool|null $network
14512 *
14513 * @return string
14514 */
14515 function checkout_url(
14516 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14517 $is_trial = false,
14518 $extra = array(),
14519 $network = null
14520 ) {
14521 $this->_logger->entrance();
14522
14523 $params = array(
14524 'checkout' => 'true',
14525 'billing_cycle' => $billing_cycle,
14526 );
14527
14528 if ( $is_trial ) {
14529 $params['trial'] = 'true';
14530 }
14531
14532 /**
14533 * Params in extra override other params.
14534 */
14535 $params = array_merge( $params, $extra );
14536
14537 return $this->_get_admin_page_url( 'pricing', $params, $network );
14538 }
14539
14540 /**
14541 * Add-on checkout URL.
14542 *
14543 * @author Vova Feldman (@svovaf)
14544 * @since 1.1.7
14545 *
14546 * @param number $addon_id
14547 * @param number $pricing_id
14548 * @param string $billing_cycle
14549 * @param bool $is_trial
14550 * @param bool|null $network
14551 *
14552 * @return string
14553 */
14554 function addon_checkout_url(
14555 $addon_id,
14556 $pricing_id,
14557 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14558 $is_trial = false,
14559 $network = null
14560 ) {
14561 return $this->checkout_url( $billing_cycle, $is_trial, array(
14562 'plugin_id' => $addon_id,
14563 'pricing_id' => $pricing_id,
14564 ), $network );
14565 }
14566
14567 #endregion
14568
14569 #endregion ------------------------------------------------------------------
14570
14571 /**
14572 * Check if plugin has any add-ons.
14573 *
14574 * @author Vova Feldman (@svovaf)
14575 * @since 1.0.5
14576 *
14577 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14578 *
14579 * @return bool
14580 */
14581 function has_addons() {
14582 $this->_logger->entrance();
14583
14584 return $this->_has_addons;
14585 }
14586
14587 /**
14588 * Check if plugin can work in anonymous mode.
14589 *
14590 * @author Vova Feldman (@svovaf)
14591 * @since 1.0.9
14592 *
14593 * @return bool
14594 *
14595 * @deprecated Please use is_enable_anonymous() instead.
14596 */
14597 function enable_anonymous() {
14598 return $this->_enable_anonymous;
14599 }
14600
14601 /**
14602 * Check if plugin can work in anonymous mode.
14603 *
14604 * @author Vova Feldman (@svovaf)
14605 * @since 1.1.9
14606 *
14607 * @return bool
14608 */
14609 function is_enable_anonymous() {
14610 return $this->_enable_anonymous;
14611 }
14612
14613 /**
14614 * Check if plugin is premium only (no free plans).
14615 *
14616 * @author Vova Feldman (@svovaf)
14617 * @since 1.1.9
14618 *
14619 * @return bool
14620 */
14621 function is_only_premium() {
14622 return $this->_is_premium_only;
14623 }
14624
14625 /**
14626 * Checks if the plugin's type is "plugin". The other type is "theme".
14627 *
14628 * @author Leo Fajardo (@leorw)
14629 * @since 1.2.2
14630 *
14631 * @return bool
14632 */
14633 function is_plugin() {
14634 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14635 }
14636
14637 /**
14638 * @author Leo Fajardo (@leorw)
14639 * @since 1.2.2
14640 *
14641 * @return string
14642 */
14643 function get_module_type() {
14644 if ( ! isset( $this->_module_type ) ) {
14645 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14646 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14647 }
14648
14649 return $this->_module_type;
14650 }
14651
14652 /**
14653 * @author Leo Fajardo (@leorw)
14654 * @since 1.2.2
14655 *
14656 * @return string
14657 */
14658 function get_plugin_main_file_path() {
14659 return $this->_plugin_main_file_path;
14660 }
14661
14662 /**
14663 * Check if module has a premium code version.
14664 *
14665 * Serviceware module might be freemium without any
14666 * premium code version, where the paid features
14667 * are all part of the service.
14668 *
14669 * @author Vova Feldman (@svovaf)
14670 * @since 1.2.1.6
14671 *
14672 * @return bool
14673 */
14674 function has_premium_version() {
14675 return $this->_has_premium_version;
14676 }
14677
14678 /**
14679 * Check if feature supported with current site's plan.
14680 *
14681 * @author Vova Feldman (@svovaf)
14682 * @since 1.0.1
14683 *
14684 * @todo IMPLEMENT
14685 *
14686 * @param number $feature_id
14687 *
14688 * @throws Exception
14689 */
14690 function is_feature_supported( $feature_id ) {
14691 throw new Exception( 'not implemented' );
14692 }
14693
14694 /**
14695 * @author Vova Feldman (@svovaf)
14696 * @since 1.0.1
14697 *
14698 * @return bool Is running in SSL/HTTPS
14699 */
14700 function is_ssl() {
14701 return WP_FS__IS_HTTPS;
14702 }
14703
14704 /**
14705 * @author Vova Feldman (@svovaf)
14706 * @since 1.0.9
14707 *
14708 * @return bool Is running in AJAX call.
14709 *
14710 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
14711 */
14712 static function is_ajax() {
14713 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
14714 }
14715
14716 /**
14717 * Check if it's an AJAX call targeted for the current module.
14718 *
14719 * @author Vova Feldman (@svovaf)
14720 * @since 1.2.0
14721 *
14722 * @param array|string $actions Collection of AJAX actions.
14723 *
14724 * @return bool
14725 */
14726 function is_ajax_action( $actions ) {
14727 // Verify it's an ajax call.
14728 if ( ! self::is_ajax() ) {
14729 return false;
14730 }
14731
14732 // Verify the call is relevant for the plugin.
14733 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
14734 return false;
14735 }
14736
14737 // Verify it's one of the specified actions.
14738 if ( is_string( $actions ) ) {
14739 $actions = explode( ',', $actions );
14740 }
14741
14742 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14743 $ajax_action = fs_request_get( 'action' );
14744
14745 foreach ( $actions as $action ) {
14746 if ( $ajax_action === $this->get_action_tag( $action ) ) {
14747 return true;
14748 }
14749 }
14750 }
14751
14752 return false;
14753 }
14754
14755 /**
14756 * Check if it's an AJAX call targeted for current request.
14757 *
14758 * @author Vova Feldman (@svovaf)
14759 * @since 1.2.0
14760 *
14761 * @param array|string $actions Collection of AJAX actions.
14762 * @param number|null $module_id
14763 *
14764 * @return bool
14765 */
14766 static function is_ajax_action_static( $actions, $module_id = null ) {
14767 // Verify it's an ajax call.
14768 if ( ! self::is_ajax() ) {
14769 return false;
14770 }
14771
14772
14773 if ( ! empty( $module_id ) ) {
14774 // Verify the call is relevant for the plugin.
14775 if ( $module_id != fs_request_get( 'module_id' ) ) {
14776 return false;
14777 }
14778 }
14779
14780 // Verify it's one of the specified actions.
14781 if ( is_string( $actions ) ) {
14782 $actions = explode( ',', $actions );
14783 }
14784
14785 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14786 $ajax_action = fs_request_get( 'action' );
14787
14788 foreach ( $actions as $action ) {
14789 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
14790 return true;
14791 }
14792 }
14793 }
14794
14795 return false;
14796 }
14797
14798 /**
14799 * @author Vova Feldman (@svovaf)
14800 * @since 1.1.7
14801 *
14802 * @return bool
14803 */
14804 static function is_cron() {
14805 return ( defined( 'DOING_CRON' ) && DOING_CRON );
14806 }
14807
14808 /**
14809 * Check if a real user is visiting the admin dashboard.
14810 *
14811 * @author Vova Feldman (@svovaf)
14812 * @since 1.1.7
14813 *
14814 * @return bool
14815 */
14816 function is_user_in_admin() {
14817 return (
14818 is_admin() &&
14819 ! self::is_ajax() &&
14820 ! self::is_cron() &&
14821 ( 'admin-post.php' !== self::get_current_page() )
14822 );
14823 }
14824
14825 /**
14826 * Check if a real user is in the customizer view.
14827 *
14828 * @author Vova Feldman (@svovaf)
14829 * @since 1.2.2.7
14830 *
14831 * @return bool
14832 */
14833 static function is_customizer() {
14834 return is_customize_preview();
14835 }
14836
14837 /**
14838 * Check if running in HTTPS and if site's plan matching the specified plan.
14839 *
14840 * @param string $plan
14841 * @param bool $exact
14842 *
14843 * @return bool
14844 */
14845 function is_ssl_and_plan( $plan, $exact = false ) {
14846 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
14847 }
14848
14849 /**
14850 * Construct plugin's settings page URL.
14851 *
14852 * @author Vova Feldman (@svovaf)
14853 * @since 1.0.4
14854 *
14855 * @param string $page
14856 * @param array $params
14857 * @param bool|null $network
14858 *
14859 * @return string
14860 */
14861 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
14862 if ( is_null( $network ) ) {
14863 $network = (
14864 $this->_is_network_active &&
14865 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
14866 );
14867 }
14868
14869 if ( 0 < count( $params ) ) {
14870 foreach ( $params as $k => $v ) {
14871 $params[ $k ] = urlencode( $v );
14872 }
14873 }
14874
14875 $page_param = $this->_menu->get_slug( $page );
14876
14877 if ( empty( $page ) &&
14878 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
14879 $this->show_opt_in_on_themes_page()
14880 ) {
14881 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
14882
14883 return add_query_arg(
14884 $params,
14885 $this->admin_url( 'themes.php', 'admin', $network )
14886 );
14887 }
14888
14889 if ( ! $this->has_settings_menu() ) {
14890 if ( ! empty( $page ) ) {
14891 // Module doesn't have a setting page, but since the request is for
14892 // a specific Freemius page, use the admin.php path.
14893 return add_query_arg( array_merge( $params, array(
14894 'page' => $page_param,
14895 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14896 } else {
14897 if ( $this->is_activation_mode() ) {
14898 /**
14899 * @author Vova Feldman
14900 * @since 1.2.1.6
14901 *
14902 * If plugin doesn't have a settings page, create one for the opt-in screen.
14903 */
14904 return add_query_arg( array_merge( $params, array(
14905 'page' => $this->_slug,
14906 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14907 } else {
14908 // Plugin without a settings page.
14909 return add_query_arg(
14910 $params,
14911 $this->admin_url( 'plugins.php', 'admin', $network )
14912 );
14913 }
14914 }
14915 }
14916
14917 // Module has a submenu settings page.
14918 if ( ! $this->_menu->is_top_level() ) {
14919 $parent_slug = $this->_menu->get_parent_slug();
14920 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
14921 $parent_slug :
14922 'admin.php';
14923
14924 return add_query_arg( array_merge( $params, array(
14925 'page' => $page_param,
14926 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
14927 }
14928
14929 // Module has a top level CPT settings page.
14930 if ( $this->_menu->is_cpt() ) {
14931 if ( empty( $page ) && $this->is_activation_mode() ) {
14932 return add_query_arg( array_merge( $params, array(
14933 'page' => $page_param
14934 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14935 } else {
14936 if ( ! empty( $page ) ) {
14937 $params['page'] = $page_param;
14938 }
14939
14940 return add_query_arg(
14941 $params,
14942 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
14943 );
14944 }
14945 }
14946
14947 // Module has a custom top level settings page.
14948 return add_query_arg( array_merge( $params, array(
14949 'page' => $page_param,
14950 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14951 }
14952
14953 #--------------------------------------------------------------------------------
14954 #region Multisite
14955 #--------------------------------------------------------------------------------
14956
14957 /**
14958 * @author Leo Fajardo (@leorw)
14959 * @since 2.0.0
14960 *
14961 * @return bool
14962 */
14963 function is_network_active() {
14964 return $this->_is_network_active;
14965 }
14966
14967 /**
14968 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
14969 *
14970 * @author Leo Fajardo (@leorw)
14971 * @since 2.0.0
14972 *
14973 * @param array|null $sites
14974 */
14975 private function delegate_connection( $sites = null ) {
14976 $this->_logger->entrance();
14977
14978 $this->_admin_notices->remove_sticky( 'connect_account' );
14979
14980 if ( is_null( $sites ) ) {
14981 // All sites delegation.
14982 $this->_storage->store( 'is_delegated_connection', true, true, true );
14983 } else {
14984 // Specified sites delegation.
14985 foreach ( $sites as $site ) {
14986 $this->delegate_site_connection( $site['blog_id'] );
14987 }
14988 }
14989
14990 $this->network_upgrade_mode_completed();
14991 }
14992
14993 /**
14994 * Delegate specific network site conncetion to the site admin.
14995 *
14996 * @author Vova Feldman (@svovaf)
14997 * @since 2.0.0
14998 *
14999 * @param int $blog_id
15000 */
15001 private function delegate_site_connection( $blog_id ) {
15002 $this->_storage->store( 'is_delegated_connection', true, $blog_id, true );
15003 }
15004
15005 /**
15006 * Check if super-admin delegated the connection of ALL sites to the site admins.
15007 *
15008 * @author Vova Feldman (@svovaf)
15009 * @since 2.0.0
15010 *
15011 * @return bool
15012 */
15013 function is_network_delegated_connection() {
15014 if ( ! $this->_is_network_active ) {
15015 return false;
15016 }
15017
15018 return $this->_storage->get( 'is_delegated_connection', false, true );
15019 }
15020
15021 /**
15022 * @author Leo Fajardo (@leorw)
15023 * @since 2.0.0
15024 *
15025 * @param int $blog_id
15026 *
15027 * @return bool
15028 */
15029 function is_site_delegated_connection( $blog_id = 0 ) {
15030 if ( ! $this->_is_network_active ) {
15031 return false;
15032 }
15033
15034 if ( 0 == $blog_id ) {
15035 $blog_id = get_current_blog_id();
15036 }
15037
15038 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15039 }
15040
15041 /**
15042 * Check if delegated the connection. When running within the the network admin,
15043 * and haven't specified the blog ID, checks if network level delegated. If running
15044 * within a site admin or specified a blog ID, check if delegated the connection for
15045 * the current context site.
15046 *
15047 * If executed outside the the admin, check if delegated the connection
15048 * for the current context site OR the whole network.
15049 *
15050 * @author Vova Feldman (@svovaf)
15051 * @since 2.0.0
15052 *
15053 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15054 *
15055 * @return bool
15056 */
15057 function is_delegated_connection( $blog_id = 0 ) {
15058 if ( ! $this->_is_network_active ) {
15059 return false;
15060 }
15061
15062 if ( fs_is_network_admin() && 0 == $blog_id ) {
15063 return $this->is_network_delegated_connection();
15064 }
15065
15066 return (
15067 $this->is_network_delegated_connection() ||
15068 $this->is_site_delegated_connection( $blog_id )
15069 );
15070 }
15071
15072 /**
15073 * Check if the current module is active for the site.
15074 *
15075 * @author Vova Feldman (@svovaf)
15076 * @since 2.0.0
15077 *
15078 * @param int $blog_id
15079 *
15080 * @return bool
15081 */
15082 function is_active_for_site( $blog_id ) {
15083 if ( ! is_multisite() ) {
15084 // Not a multisite and this code is executed, means that the plugin is active.
15085 return true;
15086 }
15087
15088 if ( $this->is_theme() ) {
15089 // All themes are site level activated.
15090 return true;
15091 }
15092
15093 if ( $this->_is_network_active ) {
15094 // Plugin was network activated so it's active.
15095 return true;
15096 }
15097
15098 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15099 }
15100
15101 /**
15102 * @author Leo Fajardo (@leorw)
15103 * @since 2.0.0
15104 *
15105 * @return array Active & public sites collection.
15106 */
15107 static function get_sites() {
15108 if ( ! is_multisite() ) {
15109 return array();
15110 }
15111
15112 /**
15113 * For consistency with get_blog_list() which only return active public sites.
15114 *
15115 * @author Vova Feldman (@svovaf)
15116 */
15117 $args = array(
15118 /**
15119 * Commented out in order to handle the migration of site options whether the site is public or not.
15120 *
15121 * @author Leo Fajardo (@leorw)
15122 * @since 2.2.1
15123 */
15124 // 'public' => 1,
15125 'archived' => 0,
15126 'mature' => 0,
15127 'spam' => 0,
15128 'deleted' => 0,
15129 );
15130
15131 if ( function_exists( 'get_sites' ) ) {
15132 // For WP 4.6 and above.
15133 return get_sites( $args );
15134 } else if ( function_exists( 'wp_' . 'get_sites' ) ) {
15135 // For WP 3.7 to WP 4.5.
15136 /**
15137 * 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.
15138 *
15139 * @todo Remove this hack once this false-positive error is removed from the Theme Sniffer.
15140 *
15141 * @since 2.3.3
15142 * @author Vova Feldman (@svovaf)
15143 */
15144 $fn = 'wp_' . 'get_sites';
15145 return $fn( $args );
15146 } else {
15147 // For WP 3.6 and below.
15148 return get_blog_list( 0, 'all' );
15149 }
15150 }
15151
15152 /**
15153 * Checks if a given blog is active.
15154 *
15155 * @author Vova Feldman (@svovaf)
15156 * @since 2.0.0
15157 *
15158 * @param $blog_id
15159 *
15160 * @return bool
15161 */
15162 private static function is_site_active( $blog_id ) {
15163 global $wpdb;
15164
15165 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15166
15167 if ( ! is_object( $blog_info ) ) {
15168 return false;
15169 }
15170
15171 return (
15172 true == $blog_info->public &&
15173 false == $blog_info->archived &&
15174 false == $blog_info->mature &&
15175 false == $blog_info->spam &&
15176 false == $blog_info->deleted
15177 );
15178 }
15179
15180 /**
15181 * Get a mapping between the site addresses to their blog IDs.
15182 *
15183 * @author Vova Feldman (@svovaf)
15184 * @since 2.0.0
15185 *
15186 * @return array {
15187 * @key string Site address without protocol with a trailing slash.
15188 * @value int Site's blog ID.
15189 * }
15190 */
15191 private function get_address_to_blog_map() {
15192 $sites = self::get_sites();
15193
15194 // Map site addresses to their blog IDs.
15195 $address_to_blog_map = array();
15196 foreach ( $sites as $site ) {
15197 $blog_id = self::get_site_blog_id( $site );
15198 $address = trailingslashit( fs_strip_url_protocol( get_site_url( $blog_id ) ) );
15199 $address_to_blog_map[ $address ] = $blog_id;
15200 }
15201
15202 return $address_to_blog_map;
15203 }
15204
15205 /**
15206 * Get a mapping between the site addresses to their blog IDs.
15207 *
15208 * @author Vova Feldman (@svovaf)
15209 * @since 2.0.0
15210 *
15211 * @return array {
15212 * @key int Site's blog ID.
15213 * @value FS_Site Associated install.
15214 * }
15215 */
15216 function get_blog_install_map() {
15217 $sites = self::get_sites();
15218
15219 // Map site blog ID to its install.
15220 $install_map = array();
15221
15222 foreach ( $sites as $site ) {
15223 $blog_id = self::get_site_blog_id( $site );
15224 $install = $this->get_install_by_blog_id( $blog_id );
15225
15226 if ( is_object( $install ) ) {
15227 $install_map[ $blog_id ] = $install;
15228 }
15229 }
15230
15231 return $install_map;
15232 }
15233
15234 /**
15235 * Gets a map of module IDs that the given user has opted-in to.
15236 *
15237 * @author Leo Fajardo (@leorw)
15238 * @since 2.1.0
15239 *
15240 * @param number $fs_user_id
15241 *
15242 * @return array {
15243 * @key number $plugin_id
15244 * @value bool Always true.
15245 * }
15246 */
15247 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15248 self::$_static_logger->entrance();
15249
15250 if ( ! is_multisite() ) {
15251 $installs = array_merge(
15252 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15253 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15254 );
15255 } else {
15256 $sites = self::get_sites();
15257
15258 $installs = array();
15259 foreach ( $sites as $site ) {
15260 $blog_id = self::get_site_blog_id( $site );
15261
15262 $installs = array_merge(
15263 $installs,
15264 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15265 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15266 );
15267 }
15268 }
15269
15270 $module_ids_map = array();
15271 foreach ( $installs as $install ) {
15272 if ( is_object( $install ) &&
15273 FS_Site::is_valid_id( $install->id ) &&
15274 FS_User::is_valid_id( $install->user_id ) &&
15275 ( $install->user_id == $fs_user_id )
15276 ) {
15277 $module_ids_map[ $install->plugin_id ] = true;
15278 }
15279 }
15280
15281 return $module_ids_map;
15282 }
15283
15284 /**
15285 * @author Leo Fajardo (@leorw)
15286 *
15287 * @return null|array {
15288 * 'install' => FS_Site Module's install,
15289 * 'blog_id' => string The associated blog ID.
15290 * }
15291 */
15292 function find_first_install() {
15293 $sites = self::get_sites();
15294
15295 foreach ( $sites as $site ) {
15296 $blog_id = self::get_site_blog_id( $site );
15297 $install = $this->get_install_by_blog_id( $blog_id );
15298
15299 if ( is_object( $install ) ) {
15300 return array(
15301 'install' => $install,
15302 'blog_id' => $blog_id
15303 );
15304 }
15305 }
15306
15307 return null;
15308 }
15309
15310 /**
15311 * Switches the Freemius site level context to a specified blog.
15312 *
15313 * @author Vova Feldman (@svovaf)
15314 * @since 2.0.0
15315 *
15316 * @param int $blog_id
15317 * @param FS_Site $install
15318 *
15319 * @return bool Since 2.3.1 returns if a switch was made.
15320 */
15321 function switch_to_blog( $blog_id, FS_Site $install = null ) {
15322 if ( ! is_numeric( $blog_id ) || $blog_id == $this->_context_is_network_or_blog_id ) {
15323 return false;
15324 }
15325
15326 switch_to_blog( $blog_id );
15327 $this->_context_is_network_or_blog_id = $blog_id;
15328
15329 self::$_accounts->set_site_blog_context( $blog_id );
15330 $this->_storage->set_site_blog_context( $blog_id );
15331 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15332
15333 $this->_site = is_object( $install ) ?
15334 $install :
15335 $this->get_install_by_blog_id( $blog_id );
15336
15337 $this->_user = false;
15338 $this->_licenses = false;
15339 $this->_license = null;
15340 $this->is_whitelabeled = null;
15341
15342 if ( is_object( $this->_site ) ) {
15343 // Try to fetch user from install.
15344 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15345
15346 if ( ! is_object( $this->_user ) &&
15347 FS_User::is_valid_id( $this->_storage->prev_user_id )
15348 ) {
15349 // Try to fetch previously saved user.
15350 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15351
15352 if ( ! is_object( $this->_user ) ) {
15353 // Fallback to network's user.
15354 $this->_user = $this->get_network_user();
15355 }
15356 }
15357
15358 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15359
15360 if ( ! empty( $all_plugin_licenses ) ) {
15361 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15362 $this->_license = null;
15363 } else {
15364 $license_found = false;
15365 foreach ( $all_plugin_licenses as $license ) {
15366 if ( $license->id == $this->_site->license_id ) {
15367 // License found.
15368 $this->_license = $license;
15369 $license_found = true;
15370 break;
15371 }
15372 }
15373
15374 if ( $license_found ) {
15375 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15376 }
15377 }
15378
15379 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15380 }
15381 }
15382
15383 unset( $this->_site_api );
15384 unset( $this->_user_api );
15385
15386 return false;
15387 }
15388
15389 /**
15390 * Restore the blog context to the blog that originally loaded the module.
15391 *
15392 * @author Vova Feldman (@svovaf)
15393 * @since 2.0.0
15394 */
15395 function restore_current_blog() {
15396 $this->switch_to_blog( $this->_blog_id );
15397 }
15398
15399 /**
15400 * @author Vova Feldman (@svovaf)
15401 * @since 2.0.0
15402 *
15403 * @param array|WP_Site $site
15404 *
15405 * @return int
15406 */
15407 static function get_site_blog_id( &$site ) {
15408 return ( $site instanceof WP_Site ) ?
15409 $site->blog_id :
15410 ( is_object( $site ) && isset( $site->userblog_id ) ?
15411 $site->userblog_id :
15412 $site['blog_id'] );
15413 }
15414
15415 /**
15416 * @author Leo Fajardo (@leorw)
15417 * @since 2.0.0
15418 *
15419 * @param array|WP_Site|null $site
15420 *
15421 * @return array
15422 */
15423 function get_site_info( $site = null ) {
15424 $this->_logger->entrance();
15425
15426 $switched = false;
15427
15428 if ( is_null( $site ) ) {
15429 $url = get_site_url();
15430 $name = get_bloginfo( 'name' );
15431 $blog_id = null;
15432 } else {
15433 $blog_id = self::get_site_blog_id( $site );
15434
15435 if ( get_current_blog_id() != $blog_id ) {
15436 switch_to_blog( $blog_id );
15437 $switched = true;
15438 }
15439
15440 if ( $site instanceof WP_Site ) {
15441 $url = $site->siteurl;
15442 $name = $site->blogname;
15443 } else {
15444 $url = get_site_url( $blog_id );
15445 $name = get_bloginfo( 'name' );
15446 }
15447 }
15448
15449 $info = array(
15450 'uid' => $this->get_anonymous_id( $blog_id ),
15451 'url' => $url,
15452 'title' => $name,
15453 'language' => get_bloginfo( 'language' ),
15454 'charset' => get_bloginfo( 'charset' ),
15455 );
15456
15457 if ( is_numeric( $blog_id ) ) {
15458 $info['blog_id'] = $blog_id;
15459 }
15460
15461 if ( $switched ) {
15462 restore_current_blog();
15463 }
15464
15465 return $info;
15466 }
15467
15468 /**
15469 * Load the module's install based on the blog ID.
15470 *
15471 * @author Vova Feldman (@svovaf)
15472 * @since 2.0.0
15473 *
15474 * @param int|null $blog_id
15475 *
15476 * @return FS_Site
15477 */
15478 function get_install_by_blog_id( $blog_id = null ) {
15479 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15480 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15481
15482 if ( is_object( $install ) &&
15483 is_numeric( $install->id ) &&
15484 is_numeric( $install->user_id ) &&
15485 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15486 ) {
15487 // Load site.
15488 $install = clone $install;
15489 }
15490
15491 return $install;
15492 }
15493
15494 /**
15495 * Check if module is installed on a specified site.
15496 *
15497 * @author Vova Feldman (@svovaf)
15498 * @since 2.0.0
15499 *
15500 * @param int|null $blog_id
15501 *
15502 * @return bool
15503 */
15504 function is_installed_on_site( $blog_id = null ) {
15505 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15506 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15507
15508 return (
15509 is_object( $install ) &&
15510 is_numeric( $install->id ) &&
15511 is_numeric( $install->user_id ) &&
15512 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15513 );
15514 }
15515
15516 /**
15517 * Check if super-admin connected at least one site via the network opt-in.
15518 *
15519 * @author Vova Feldman (@svovaf)
15520 * @since 2.0.0
15521 *
15522 * @return bool
15523 */
15524 function is_network_registered() {
15525 if ( ! $this->_is_network_active ) {
15526 return false;
15527 }
15528
15529 return FS_User::is_valid_id( $this->_storage->network_user_id );
15530 }
15531
15532 /**
15533 * Returns the main user associated with the network.
15534 *
15535 * @author Vova Feldman (@svovaf)
15536 * @since 2.0.0
15537 *
15538 * @return FS_User
15539 */
15540 function get_network_user() {
15541 if ( ! $this->_is_network_active ) {
15542 return null;
15543 }
15544
15545 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15546 self::_get_user_by_id( $this->_storage->network_user_id ) :
15547 null;
15548 }
15549
15550 /**
15551 * Returns the current context user or the network's main user.
15552 *
15553 * @author Vova Feldman (@svovaf)
15554 * @since 2.0.0
15555 *
15556 * @return FS_User
15557 */
15558 function get_current_or_network_user() {
15559 return ( $this->_user instanceof FS_User ) ?
15560 $this->_user :
15561 $this->get_network_user();
15562 }
15563
15564 /**
15565 * Returns the main install associated with the network.
15566 *
15567 * @author Vova Feldman (@svovaf)
15568 * @since 2.0.0
15569 *
15570 * @return FS_Site
15571 */
15572 function get_network_install() {
15573 if ( ! $this->_is_network_active ) {
15574 return null;
15575 }
15576
15577 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15578 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15579 null;
15580 }
15581
15582 /**
15583 * Returns the blog ID that is associated with the main install.
15584 *
15585 * @author Leo Fajardo (@leorw)
15586 * @since 2.0.0
15587 *
15588 * @return int|null
15589 */
15590 function get_network_install_blog_id() {
15591 if ( ! $this->_is_network_active ) {
15592 return null;
15593 }
15594
15595 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15596 $this->_storage->network_install_blog_id :
15597 null;
15598 }
15599
15600 /**
15601 * Returns the current context install or the network's main install.
15602 *
15603 * @author Vova Feldman (@svovaf)
15604 * @since 2.0.0
15605 *
15606 * @return FS_Site
15607 */
15608 function get_current_or_network_install() {
15609 return ( $this->_site instanceof FS_Site ) ?
15610 $this->_site :
15611 $this->get_network_install();
15612 }
15613
15614 /**
15615 * Check if executing a site level action from the network level admin.
15616 *
15617 * @author Vova Feldman (@svovaf)
15618 * @since 2.0.0
15619 *
15620 * @return false|int If yes, return the requested blog ID.
15621 */
15622 private function is_network_level_site_specific_action() {
15623 if ( ! $this->_is_network_active ) {
15624 return false;
15625 }
15626
15627 if ( ! fs_is_network_admin() ) {
15628 return false;
15629 }
15630
15631 $blog_id = fs_request_get( 'blog_id', '' );
15632
15633 return is_numeric( $blog_id ) ? $blog_id : false;
15634 }
15635
15636 /**
15637 * Check if executing an action from the network level admin.
15638 *
15639 * @author Vova Feldman (@svovaf)
15640 * @since 2.0.0
15641 *
15642 * @return bool
15643 */
15644 private function is_network_level_action() {
15645 return ( $this->_is_network_active && fs_is_network_admin() );
15646 }
15647
15648 /**
15649 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
15650 * 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.
15651 *
15652 * @author Vova Feldman (@svovaf)
15653 * @since 2.0.0
15654 *
15655 * @param int $context_blog_id
15656 */
15657 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
15658 $this->_logger->entrance();
15659
15660 if ( $this->_is_network_active ) {
15661 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
15662 $installs_map = $this->get_blog_install_map();
15663
15664 foreach ( $installs_map as $blog_id => $install ) {
15665 /**
15666 * @var FS_Site $install
15667 */
15668 if ( $context_blog_id == $blog_id ) {
15669 continue;
15670 }
15671
15672 if ( $install->user_id != $this->_storage->network_user_id ) {
15673 continue;
15674 }
15675
15676 // Switch reference to a blog that is opted-in and belong to the same super-admin.
15677 $this->_storage->network_install_blog_id = $blog_id;
15678 break;
15679 }
15680 }
15681 }
15682
15683 if ( $this->is_sync_cron_scheduled() &&
15684 $context_blog_id == $this->get_sync_cron_blog_id()
15685 ) {
15686 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
15687 }
15688
15689 if ( $this->is_install_sync_scheduled() &&
15690 $context_blog_id == $this->get_install_sync_cron_blog_id()
15691 ) {
15692 $this->schedule_install_sync( $context_blog_id );
15693 }
15694 }
15695
15696 /**
15697 * Executed after site deactivation, archive, or flag as spam.
15698 *
15699 * @author Vova Feldman (@svovaf)
15700 * @since 2.0.0
15701 *
15702 * @param int $context_blog_id
15703 */
15704 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
15705 $this->_logger->entrance();
15706
15707 $install = $this->get_install_by_blog_id( $context_blog_id );
15708
15709 if ( ! is_object( $install ) ) {
15710 // Site not connected.
15711 return;
15712 }
15713
15714 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15715
15716 $current_blog_id = get_current_blog_id();
15717
15718 $this->switch_to_blog( $context_blog_id );
15719
15720 // Send deactivation event.
15721 $this->sync_install( array(
15722 'is_active' => false,
15723 ) );
15724
15725 $this->switch_to_blog( $current_blog_id );
15726 }
15727
15728 /**
15729 * Executed after site deletion.
15730 *
15731 * @author Vova Feldman (@svovaf)
15732 * @since 2.0.0
15733 *
15734 * @param int $context_blog_id
15735 * @param bool $drop True if site's database tables should be dropped. Default is false.
15736 */
15737 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
15738 $this->_logger->entrance();
15739
15740 $install = $this->get_install_by_blog_id( $context_blog_id );
15741
15742 if ( ! is_object( $install ) ) {
15743 // Site not connected.
15744 return;
15745 }
15746
15747 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15748
15749 $current_blog_id = get_current_blog_id();
15750
15751 $this->switch_to_blog( $context_blog_id );
15752
15753 if ( $drop ) {
15754 // Delete install if dropping site DB.
15755 $this->delete_account_event();
15756 } else {
15757 // Send deactivation event.
15758 $this->sync_install( array(
15759 'is_active' => false,
15760 ) );
15761 }
15762
15763 $this->switch_to_blog( $current_blog_id );
15764 }
15765
15766 /**
15767 * Executed after site re-activation.
15768 *
15769 * @author Vova Feldman (@svovaf)
15770 * @since 2.0.0
15771 *
15772 * @param int $context_blog_id
15773 */
15774 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
15775 $this->_logger->entrance();
15776
15777 $install = $this->get_install_by_blog_id( $context_blog_id );
15778
15779 if ( ! is_object( $install ) ) {
15780 // Site not connected.
15781 return;
15782 }
15783
15784 if ( ! self::is_site_active( $context_blog_id ) ) {
15785 // Site not yet active (can be in spam mode, archived, deleted...).
15786 return;
15787 }
15788
15789 $current_blog_id = get_current_blog_id();
15790
15791 $this->switch_to_blog( $context_blog_id );
15792
15793 // Send re-activation event.
15794 $this->sync_install( array(
15795 'is_active' => true,
15796 ) );
15797
15798 $this->switch_to_blog( $current_blog_id );
15799 }
15800
15801 #endregion Multisite
15802
15803 /**
15804 * @author Leo Fajardo (@leorw)
15805 *
15806 * @param string $path
15807 * @param string $scheme
15808 * @param bool $network
15809 *
15810 * @return string
15811 */
15812 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
15813 return ( $this->_is_network_active && $network ) ?
15814 network_admin_url( $path, $scheme ) :
15815 admin_url( $path, $scheme );
15816 }
15817
15818 /**
15819 * Check if currently in a specified admin page.
15820 *
15821 * @author Vova Feldman (@svovaf)
15822 * @since 1.2.2.7
15823 *
15824 * @param string $page
15825 *
15826 * @return bool
15827 */
15828 function is_admin_page( $page ) {
15829 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
15830 }
15831
15832 /**
15833 * Check if currently in the product's main admin page.
15834 *
15835 * @author Vova Feldman (@svovaf)
15836 * @since 2.3.1
15837 *
15838 * @return bool
15839 */
15840 function is_main_admin_page() {
15841 return $this->is_admin_page( '' );
15842 }
15843
15844 /**
15845 * Get module's main admin setting page URL.
15846 *
15847 * @author Vova Feldman (@svovaf)
15848 * @since 1.2.2.7
15849 *
15850 * @return string
15851 */
15852 function main_menu_url() {
15853 return $this->_menu->main_menu_url();
15854 }
15855
15856 /**
15857 * Check if currently on the theme's setting page or
15858 * on any of the Freemius added pages (via tabs).
15859 *
15860 * @author Vova Feldman (@svovaf)
15861 * @since 1.2.2.7
15862 *
15863 * @return bool
15864 *
15865 * @deprecated Please use is_product_settings_page() instead;
15866 */
15867 function is_theme_settings_page() {
15868 return $this->is_product_settings_page();
15869 }
15870
15871 /**
15872 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
15873 *
15874 * @author Vova Feldman (@svovaf)
15875 * @since 1.2.2.7
15876 *
15877 * @return bool
15878 */
15879 function is_product_settings_page() {
15880 return fs_starts_with(
15881 fs_request_get( 'page', '', 'get' ),
15882 $this->_menu->get_slug()
15883 );
15884 }
15885
15886 /**
15887 * Plugin's account page + sync license URL.
15888 *
15889 * @author Vova Feldman (@svovaf)
15890 * @since 1.1.9.1
15891 *
15892 * @param bool|number $plugin_id
15893 * @param bool $add_action_nonce
15894 * @param array $params
15895 *
15896 * @return string
15897 */
15898 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
15899 if ( is_numeric( $plugin_id ) ) {
15900 $params['plugin_id'] = $plugin_id;
15901 }
15902
15903 return $this->get_account_url(
15904 $this->get_unique_affix() . '_sync_license',
15905 $params,
15906 $add_action_nonce
15907 );
15908 }
15909
15910 /**
15911 * Plugin's account URL.
15912 *
15913 * @author Vova Feldman (@svovaf)
15914 * @since 1.0.4
15915 *
15916 * @param bool|string $action
15917 * @param array $params
15918 *
15919 * @param bool $add_action_nonce
15920 *
15921 * @return string
15922 */
15923 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
15924 if ( is_string( $action ) ) {
15925 $params['fs_action'] = $action;
15926 }
15927
15928 self::require_pluggable_essentials();
15929
15930 return ( $add_action_nonce && is_string( $action ) ) ?
15931 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
15932 $this->_get_admin_page_url( 'account', $params );
15933 }
15934
15935 /**
15936 * @author Vova Feldman (@svovaf)
15937 * @since 1.2.0
15938 *
15939 * @param string $tab
15940 * @param bool $action
15941 * @param array $params
15942 * @param bool $add_action_nonce
15943 *
15944 * @return string
15945 *
15946 * @uses get_account_url()
15947 */
15948 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
15949 $params['tab'] = $tab;
15950
15951 return $this->get_account_url( $action, $params, $add_action_nonce );
15952 }
15953
15954 /**
15955 * Plugin's account URL.
15956 *
15957 * @author Vova Feldman (@svovaf)
15958 * @since 1.0.4
15959 *
15960 * @param bool|string $topic
15961 * @param bool|string $message
15962 *
15963 * @return string
15964 */
15965 function contact_url( $topic = false, $message = false ) {
15966 $params = array();
15967 if ( is_string( $topic ) ) {
15968 $params['topic'] = $topic;
15969 }
15970 if ( is_string( $message ) ) {
15971 $params['message'] = $message;
15972 }
15973
15974 if ( $this->is_addon() ) {
15975 $params['addon_id'] = $this->get_id();
15976
15977 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
15978 } else {
15979 return $this->_get_admin_page_url( 'contact', $params );
15980 }
15981 }
15982
15983 /**
15984 * Add-on direct info URL.
15985 *
15986 * @author Vova Feldman (@svovaf)
15987 * @since 1.1.0
15988 *
15989 * @param string $slug
15990 *
15991 * @return string
15992 */
15993 function addon_url( $slug ) {
15994 return $this->_get_admin_page_url( 'addons', array(
15995 'slug' => $slug
15996 ) );
15997 }
15998
15999 /**
16000 * Add-ons URL.
16001 *
16002 * @author Vova Feldman (@svovaf)
16003 * @since 2.4.5
16004 *
16005 * @return string
16006 */
16007 function get_addons_url() {
16008 return $this->_get_admin_page_url( 'addons' );
16009 }
16010
16011 /* Logger
16012 ------------------------------------------------------------------------------------------------------------------*/
16013 /**
16014 * @param string $id
16015 * @param bool $prefix_slug
16016 *
16017 * @return FS_Logger
16018 */
16019 function get_logger( $id = '', $prefix_slug = true ) {
16020 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16021 }
16022
16023 /**
16024 * Note: This method is used externally so don't delete it.
16025 *
16026 * @param $id
16027 * @param bool $load_options
16028 * @param bool $prefix_slug
16029 *
16030 * @return FS_Option_Manager
16031 */
16032 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16033 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16034 }
16035
16036 /* Security
16037 ------------------------------------------------------------------------------------------------------------------*/
16038 private static function _encrypt( $str ) {
16039 if ( is_null( $str ) ) {
16040 return null;
16041 }
16042
16043 /**
16044 * The encrypt/decrypt functions are used to protect
16045 * the user from messing up with some of the sensitive
16046 * data stored for the module as a JSON in the database.
16047 *
16048 * I used the same suggested hack by the theme review team.
16049 * For more details, look at the function `Base64UrlDecode()`
16050 * in `./sdk/FreemiusBase.php`.
16051 *
16052 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16053 *
16054 * @author Vova Feldman (@svovaf)
16055 * @since 1.2.2
16056 */
16057 $fn = 'base64' . '_encode';
16058
16059 return $fn( $str );
16060 }
16061
16062 static function _decrypt( $str ) {
16063 if ( is_null( $str ) ) {
16064 return null;
16065 }
16066
16067 /**
16068 * The encrypt/decrypt functions are used to protect
16069 * the user from messing up with some of the sensitive
16070 * data stored for the module as a JSON in the database.
16071 *
16072 * I used the same suggested hack by the theme review team.
16073 * For more details, look at the function `Base64UrlDecode()`
16074 * in `./sdk/FreemiusBase.php`.
16075 *
16076 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16077 *
16078 * @author Vova Feldman (@svovaf)
16079 * @since 1.2.2
16080 */
16081 $fn = 'base64' . '_decode';
16082
16083 return $fn( $str );
16084 }
16085
16086 /**
16087 * @author Vova Feldman (@svovaf)
16088 * @since 1.0.5
16089 *
16090 * @param FS_Entity $entity
16091 *
16092 * @return FS_Entity Return an encrypted clone entity.
16093 */
16094 private static function _encrypt_entity( FS_Entity $entity ) {
16095 $clone = clone $entity;
16096 $props = get_object_vars( $entity );
16097
16098 foreach ( $props as $key => $val ) {
16099 $clone->{$key} = self::_encrypt( $val );
16100 }
16101
16102 return $clone;
16103 }
16104
16105 /**
16106 * @author Vova Feldman (@svovaf)
16107 * @since 1.0.5
16108 *
16109 * @param FS_Entity $entity
16110 *
16111 * @return FS_Entity Return an decrypted clone entity.
16112 */
16113 private static function decrypt_entity( FS_Entity $entity ) {
16114 $clone = clone $entity;
16115 $props = get_object_vars( $entity );
16116
16117 foreach ( $props as $key => $val ) {
16118 $clone->{$key} = self::_decrypt( $val );
16119 }
16120
16121 return $clone;
16122 }
16123
16124 /**
16125 * Tries to activate account based on POST params.
16126 *
16127 * @author Vova Feldman (@svovaf)
16128 * @since 1.0.2
16129 *
16130 * @deprecated Not in use, outdated.
16131 */
16132 function _activate_account() {
16133 if ( $this->is_registered() ) {
16134 // Already activated.
16135 return;
16136 }
16137
16138 self::_clean_admin_content_section();
16139
16140 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
16141 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
16142
16143 // Verify matching plugin details.
16144 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
16145 return;
16146 }
16147
16148 $user = new FS_User();
16149 $user->id = fs_request_get( 'user_id' );
16150 $user->public_key = fs_request_get( 'user_public_key' );
16151 $user->secret_key = fs_request_get( 'user_secret_key' );
16152 $user->email = fs_request_get( 'user_email' );
16153 $user->first = fs_request_get( 'user_first' );
16154 $user->last = fs_request_get( 'user_last' );
16155 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
16156
16157 $site = new FS_Site();
16158 $site->id = fs_request_get( 'install_id' );
16159 $site->public_key = fs_request_get( 'install_public_key' );
16160 $site->secret_key = fs_request_get( 'install_secret_key' );
16161 $site->plan_id = fs_request_get( 'plan_id' );
16162
16163 $plans = array();
16164 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
16165 foreach ( $plans_data as $p ) {
16166 $plan = new FS_Plugin_Plan( $p );
16167 if ( $site->plan_id == $plan->id ) {
16168 $plan->title = fs_request_get( 'plan_title' );
16169 $plan->name = fs_request_get( 'plan_name' );
16170 }
16171
16172 $plans[] = $plan;
16173 }
16174
16175 $this->_set_account( $user, $site, $plans );
16176
16177 // Reload the page with the keys.
16178 fs_redirect( $this->_get_admin_page_url() );
16179 }
16180 }
16181
16182 /**
16183 * @author Vova Feldman (@svovaf)
16184 * @since 1.0.7
16185 *
16186 * @param string $email
16187 *
16188 * @return FS_User|false
16189 */
16190 static function _get_user_by_email( $email ) {
16191 self::$_static_logger->entrance();
16192
16193 $email = trim( strtolower( $email ) );
16194
16195 $users = self::get_all_users();
16196
16197 if ( is_array( $users ) ) {
16198 foreach ( $users as $user ) {
16199 if ( $email === trim( strtolower( $user->email ) ) ) {
16200 return $user;
16201 }
16202 }
16203 }
16204
16205 return false;
16206 }
16207
16208 #----------------------------------------------------------------------------------
16209 #region Account (Loading, Updates & Activation)
16210 #----------------------------------------------------------------------------------
16211
16212 /***
16213 * Load account information (user + site).
16214 *
16215 * @author Vova Feldman (@svovaf)
16216 * @since 1.0.1
16217 */
16218 private function _load_account() {
16219 $this->_logger->entrance();
16220
16221 $this->do_action( 'before_account_load' );
16222
16223 $users = self::get_all_users();
16224 $plans = self::get_all_plans( $this->_module_type );
16225
16226 if ( $this->_logger->is_on() && is_admin() ) {
16227 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16228 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16229 }
16230
16231 $site = fs_is_network_admin() ?
16232 $this->get_network_install() :
16233 $this->get_install_by_blog_id();
16234
16235 if ( fs_is_network_admin() &&
16236 $this->is_network_active() &&
16237 ! is_object( $site ) &&
16238 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16239 ) {
16240 $first_install = $this->find_first_install();
16241
16242 if ( is_null( $first_install ) ) {
16243 unset( $this->_storage->network_install_blog_id );
16244 } else {
16245 $site = $first_install['install'];
16246 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16247 }
16248 }
16249
16250 if ( is_object( $site ) &&
16251 is_numeric( $site->id ) &&
16252 is_numeric( $site->user_id ) &&
16253 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16254 ) {
16255 // Load site.
16256 $this->_site = $site;
16257
16258 // Load plans.
16259 $this->_plans = $plans[ $this->_slug ];
16260 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16261 $this->_sync_plans();
16262 } else {
16263 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16264 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16265 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16266 } else {
16267 unset( $this->_plans[ $i ] );
16268 }
16269 }
16270 }
16271 }
16272
16273 $user = null;
16274 if ( fs_is_network_admin() && $this->_is_network_active ) {
16275 $user = $this->get_network_user();
16276 }
16277
16278 if ( is_object( $user ) ) {
16279 $this->_user = clone $user;
16280 } else if ( $this->_site ) {
16281 $user = self::_get_user_by_id( $this->_site->user_id );
16282
16283 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16284 /**
16285 * Try to load the previous owner. This recovery is used for the following use-case:
16286 * 1. Opt-in
16287 * 2. Cloning site1 to site2
16288 * 3. Ownership switch in site1 (same applies for site2)
16289 * 4. Install data sync on site2
16290 * 5. Now site2's install is associated with the new owner which does not exists locally.
16291 */
16292 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16293 }
16294
16295 if ( ! is_object( $user ) ) {
16296 /**
16297 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16298 */
16299 $user = $this->sync_user_by_current_install();
16300 }
16301
16302 $this->_user = ( $user instanceof FS_User ) ?
16303 clone $user :
16304 null;
16305 }
16306
16307 if ( is_object( $this->_user ) ) {
16308 // Load licenses.
16309 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16310 }
16311
16312 if ( is_object( $this->_site ) ) {
16313 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16314
16315 if ( $this->_site->version != $this->get_plugin_version() ) {
16316 // If stored install version is different than current installed plugin version,
16317 // then update plugin version event.
16318 $this->update_plugin_version_event();
16319 }
16320 }
16321
16322 if ( true === $this->_storage->require_license_activation &&
16323 ! fs_request_get_bool( 'require_license', true )
16324 ) {
16325 $this->_storage->require_license_activation = false;
16326 }
16327
16328 if ( $this->is_theme() ) {
16329 $this->_register_account_hooks();
16330 }
16331 }
16332
16333 /**
16334 * Special user recovery mechanism.
16335 *
16336 * @author Vova Feldman (@svovaf)
16337 * @since 2.0.0
16338 *
16339 * @param number|null $site_user_id
16340 *
16341 * @return \FS_User|mixed
16342 */
16343 private function sync_user_by_current_install( $site_user_id = null ) {
16344 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16345 $site_user_id :
16346 $this->_site->user_id;
16347
16348 $api = $this->get_api_site_scope();
16349
16350 $uid = $this->get_anonymous_id();
16351 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16352
16353 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16354
16355 if ( $this->is_api_result_entity( $result ) ) {
16356 $user = new FS_User( $result );
16357 $this->_user = $user;
16358 $this->_store_user();
16359
16360 return $user;
16361 }
16362
16363 $error_code = FS_Api::get_error_code( $result );
16364
16365 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16366 /**
16367 * Those API errors will continue coming and are not recoverable with the
16368 * current site's data. Therefore, extend the API call's cached result to 7 days.
16369 */
16370 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16371 }
16372
16373 return $result;
16374 }
16375
16376 /**
16377 * @author Vova Feldman (@svovaf)
16378 * @since 1.0.1
16379 *
16380 * @param FS_User $user
16381 * @param FS_Site $site
16382 * @param bool|array $plans
16383 */
16384 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16385 $site->user_id = $user->id;
16386
16387 $this->_site = $site;
16388 $this->_user = $user;
16389 if ( false !== $plans ) {
16390 $this->_plans = $plans;
16391 }
16392
16393 $this->send_install_update();
16394
16395 $this->_store_account();
16396
16397 }
16398
16399 /**
16400 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16401 * Each version is trimmed after the 16th char.
16402 *
16403 * @author Vova Feldman (@svovaf)
16404 * @since 2.2.1
16405 *
16406 * @return array
16407 */
16408 private function get_versions() {
16409 $versions = array();
16410 $versions['platform_version'] = get_bloginfo( 'version' );
16411 $versions['sdk_version'] = $this->version;
16412 $versions['programming_language_version'] = phpversion();
16413
16414 foreach ( $versions as $k => $version ) {
16415 if ( is_string( $versions[ $k ] ) && ! empty( $versions[ $k ] ) ) {
16416 $versions[ $k ] = substr( $versions[ $k ], 0, 16 );
16417 }
16418 }
16419
16420 return $versions;
16421 }
16422
16423 /**
16424 * @author Leo Fajardo (@leorw)
16425 * @since 2.3.0
16426 *
16427 * @return bool
16428 */
16429 function has_beta_update() {
16430 return (
16431 ! empty( $this->_storage->beta_data ) &&
16432 ( true === $this->_storage->beta_data['is_beta'] ) &&
16433 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16434 );
16435 }
16436
16437 /**
16438 * @author Leo Fajardo (@leorw)
16439 * @since 2.3.0
16440 *
16441 * @return bool
16442 */
16443 function is_beta() {
16444 return (
16445 ! empty( $this->_storage->beta_data ) &&
16446 ( true === $this->_storage->beta_data['is_beta'] ) &&
16447 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16448 );
16449 }
16450
16451 /**
16452 * @author Leo Fajardo (@leorw)
16453 * @since 2.3.0
16454 */
16455 private function sync_user_beta_mode() {
16456 $user = $this->get_api_user_scope()->get( '/?plugin_id=' . $this->get_id() . '&fields=is_beta' );
16457
16458 if ( $this->is_api_result_entity( $user ) ) {
16459 $this->_user->is_beta = $user->is_beta;
16460 $this->_store_user();
16461 }
16462 }
16463
16464 /**
16465 * @author Vova Feldman (@svovaf)
16466 * @since 1.1.7.4
16467 *
16468 * @param array $override_with
16469 * @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.
16470 *
16471 * @return array
16472 */
16473 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
16474 $this->_logger->entrance();
16475
16476 $current_user = self::_get_current_wp_user();
16477
16478 $activation_action = $this->get_unique_affix() . '_activate_new';
16479 $return_url = $this->is_anonymous() ?
16480 // If skipped already, then return to the account page.
16481 $this->get_account_url( $activation_action, array(), false ) :
16482 // Return to the module's main page.
16483 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16484
16485 $versions = $this->get_versions();
16486
16487 $params = array_merge( $versions, array(
16488 'user_firstname' => $current_user->user_firstname,
16489 'user_lastname' => $current_user->user_lastname,
16490 'user_nickname' => $current_user->user_nicename,
16491 'user_email' => $current_user->user_email,
16492 'user_ip' => WP_FS__REMOTE_ADDR,
16493 'plugin_slug' => $this->_slug,
16494 'plugin_id' => $this->get_id(),
16495 'plugin_public_key' => $this->get_public_key(),
16496 'plugin_version' => $this->get_plugin_version(),
16497 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16498 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16499 'account',
16500 array( 'fs_action' => 'sync_user' )
16501 ), 'sync_user' ),
16502 'is_premium' => $this->is_premium(),
16503 'is_active' => true,
16504 'is_uninstalled' => false,
16505 ) );
16506
16507 if ( $this->is_addon() ) {
16508 $parent_fs = $this->get_parent_instance();
16509
16510 $params['parent_plugin_slug'] = $parent_fs->_slug;
16511 $params['parent_plugin_id'] = $parent_fs->get_id();
16512 }
16513
16514 if ( true === $network_level_or_blog_id ) {
16515 if ( ! isset( $override_with['sites'] ) ) {
16516 $params['sites'] = $this->get_sites_for_network_level_optin();
16517 }
16518 } else {
16519 $site = is_numeric( $network_level_or_blog_id ) ?
16520 array( 'blog_id' => $network_level_or_blog_id ) :
16521 null;
16522
16523 $site = $this->get_site_info( $site );
16524
16525 $params = array_merge( $params, array(
16526 'site_uid' => $site['uid'],
16527 'site_url' => $site['url'],
16528 'site_name' => $site['title'],
16529 'language' => $site['language'],
16530 'charset' => $site['charset'],
16531 ) );
16532 }
16533
16534 if ( $this->is_pending_activation() &&
16535 ! empty( $this->_storage->pending_license_key )
16536 ) {
16537 $params['license_key'] = $this->_storage->pending_license_key;
16538 }
16539
16540 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
16541 // Even though rand() is known for its security issues,
16542 // the timestamp adds another layer of protection.
16543 // It would be very hard for an attacker to get the secret key form here.
16544 // Plus, this should never run in production since the secret should never
16545 // be included in the production version.
16546 $params['ts'] = WP_FS__SCRIPT_START_TIME;
16547 $params['salt'] = md5( uniqid( rand() ) );
16548 $params['secure'] = md5(
16549 $params['ts'] .
16550 $params['salt'] .
16551 $this->get_secret_key()
16552 );
16553 }
16554
16555 return array_merge( $params, $override_with );
16556 }
16557
16558 /**
16559 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
16560 * 2. If there was an API error, return the API result.
16561 *
16562 * @author Vova Feldman (@svovaf)
16563 * @since 1.1.7.4
16564 *
16565 * @param string|bool $email
16566 * @param string|bool $first
16567 * @param string|bool $last
16568 * @param string|bool $license_key
16569 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
16570 * In this case, the user and site info will be sent to the server but no
16571 * data will be saved to the WP installation's database.
16572 * @param number|bool $trial_plan_id
16573 * @param bool $is_disconnected Whether or not to opt in without tracking.
16574 * @param null|bool $is_marketing_allowed
16575 * @param array $sites If network-level opt-in, an array of containing details of sites.
16576 *
16577 * @return string|object
16578 * @use WP_Error
16579 */
16580 function opt_in(
16581 $email = false,
16582 $first = false,
16583 $last = false,
16584 $license_key = false,
16585 $is_uninstall = false,
16586 $trial_plan_id = false,
16587 $is_disconnected = false,
16588 $is_marketing_allowed = null,
16589 $sites = array()
16590 ) {
16591 $this->_logger->entrance();
16592
16593 if ( false === $email ) {
16594 $current_user = self::_get_current_wp_user();
16595 $email = $current_user->user_email;
16596 }
16597
16598 /**
16599 * @since 1.2.1 If activating with license key, ignore the context-user
16600 * since the user will be automatically loaded from the license.
16601 */
16602 if ( empty( $license_key ) ) {
16603 // Clean up pending license if opt-ing in again.
16604 $this->_storage->remove( 'pending_license_key' );
16605
16606 if ( ! $is_uninstall ) {
16607 $fs_user = Freemius::_get_user_by_email( $email );
16608 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
16609 return $this->install_with_user(
16610 $fs_user,
16611 false,
16612 $trial_plan_id,
16613 true,
16614 true,
16615 $sites
16616 );
16617 }
16618 }
16619 }
16620
16621 $user_info = array();
16622 if ( ! empty( $email ) ) {
16623 $user_info['user_email'] = $email;
16624 }
16625 if ( ! empty( $first ) ) {
16626 $user_info['user_firstname'] = $first;
16627 }
16628 if ( ! empty( $last ) ) {
16629 $user_info['user_lastname'] = $last;
16630 }
16631
16632 if ( ! empty( $sites ) ) {
16633 $is_network = true;
16634
16635 $user_info['sites'] = $sites;
16636 } else {
16637 $is_network = false;
16638 }
16639
16640 $params = $this->get_opt_in_params( $user_info, $is_network );
16641
16642 $filtered_license_key = false;
16643 if ( is_string( $license_key ) ) {
16644 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
16645 $params['license_key'] = $filtered_license_key;
16646 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
16647 $params['trial_plan_id'] = $trial_plan_id;
16648 }
16649
16650 if ( $is_uninstall ) {
16651 $params['uninstall_params'] = array(
16652 'reason_id' => $this->_storage->uninstall_reason->id,
16653 'reason_info' => $this->_storage->uninstall_reason->info
16654 );
16655 }
16656
16657 if ( isset( $params['license_key'] ) ) {
16658 $fs_user = Freemius::_get_user_by_email( $email );
16659
16660 if ( is_object( $fs_user ) ) {
16661 /**
16662 * If opting in with a context license and the context WP Admin user already opted in
16663 * before from the current site, add the user context security params to avoid the
16664 * unnecessary email activation when the context license is owned by the same context user.
16665 *
16666 * @author Leo Fajardo (@leorw)
16667 * @since 1.2.3
16668 */
16669 $params = array_merge( $params, FS_Security::instance()->get_context_params(
16670 $fs_user,
16671 false,
16672 'install_with_existing_user'
16673 ) );
16674 }
16675 }
16676
16677 if ( is_bool( $is_marketing_allowed ) ) {
16678 $params['is_marketing_allowed'] = $is_marketing_allowed;
16679 }
16680
16681 $params['is_disconnected'] = $is_disconnected;
16682 $params['format'] = 'json';
16683
16684 $request = array(
16685 'method' => 'POST',
16686 'body' => $params,
16687 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
16688 );
16689
16690 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
16691 $response = self::safe_remote_post( $url, $request );
16692
16693 if ( is_wp_error( $response ) ) {
16694 /**
16695 * @var WP_Error $response
16696 */
16697 $result = new stdClass();
16698
16699 $error_code = $response->get_error_code();
16700 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
16701
16702 $result->error = (object) array(
16703 'type' => $error_type,
16704 'message' => $response->get_error_message(),
16705 'code' => $error_code,
16706 'http' => 402
16707 );
16708
16709 $this->maybe_modify_api_curl_error_message( $result );
16710
16711 return $result;
16712 }
16713
16714 // Module is being uninstalled, don't handle the returned data.
16715 if ( $is_uninstall ) {
16716 return true;
16717 }
16718
16719 /**
16720 * 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.
16721 *
16722 * @author Vova Feldman (@svovaf)
16723 * @since 1.2.3
16724 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
16725 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
16726 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
16727 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
16728 */
16729 $decoded = is_string( $response['body'] ) ?
16730 json_decode( $response['body'] ) :
16731 null;
16732
16733 if ( empty( $decoded ) ) {
16734 return false;
16735 }
16736
16737 if ( ! $this->is_api_result_object( $decoded ) ) {
16738 if ( ! empty( $params['license_key'] ) ) {
16739 // Pass the fully entered license key to the failure handler.
16740 $params['license_key'] = $license_key;
16741 }
16742
16743 return $is_uninstall ?
16744 $decoded :
16745 $this->apply_filters( 'after_install_failure', $decoded, $params );
16746 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
16747 if ( $is_network ) {
16748 $site_ids = array();
16749 foreach ( $sites as $site ) {
16750 $site_ids[] = $site['blog_id'];
16751 }
16752
16753 /**
16754 * Store the sites so that they can be installed once the user has clicked on the activation link
16755 * in the email.
16756 *
16757 * @author Leo Fajardo (@leorw)
16758 */
16759 $this->_storage->pending_sites_info = array(
16760 'blog_ids' => $site_ids,
16761 'license_key' => $license_key,
16762 'trial_plan_id' => $trial_plan_id
16763 );
16764 }
16765
16766 // Pending activation, add message.
16767 return $this->set_pending_confirmation(
16768 ( isset( $decoded->email ) ?
16769 $decoded->email :
16770 true ),
16771 false,
16772 $filtered_license_key,
16773 ! empty( $params['trial_plan_id'] )
16774 );
16775 } else if ( isset( $decoded->install_secret_key ) ) {
16776 return $this->install_with_new_user(
16777 $decoded->user_id,
16778 $decoded->user_public_key,
16779 $decoded->user_secret_key,
16780 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
16781 $decoded->is_marketing_allowed :
16782 null ),
16783 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
16784 $decoded->is_extensions_tracking_allowed :
16785 null ),
16786 $decoded->install_id,
16787 $decoded->install_public_key,
16788 $decoded->install_secret_key,
16789 false
16790 );
16791 } else if ( is_array( $decoded->installs ) ) {
16792 return $this->install_many_with_new_user(
16793 $decoded->user_id,
16794 $decoded->user_public_key,
16795 $decoded->user_secret_key,
16796 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
16797 $decoded->is_marketing_allowed :
16798 null ),
16799 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
16800 $decoded->is_extensions_tracking_allowed :
16801 null ),
16802 $decoded->installs,
16803 false
16804 );
16805 }
16806
16807 return $decoded;
16808 }
16809
16810 /**
16811 * Set user and site identities.
16812 *
16813 * @author Vova Feldman (@svovaf)
16814 * @since 1.0.9
16815 *
16816 * @param FS_User $user
16817 * @param FS_Site $site
16818 * @param bool $redirect
16819 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
16820 * redirect (or return a URL) to the account page with a special parameter to
16821 * trigger the auto installation processes.
16822 *
16823 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16824 */
16825 function setup_account(
16826 FS_User $user,
16827 FS_Site $site,
16828 $redirect = true,
16829 $auto_install = false
16830 ) {
16831 return $this->setup_network_account(
16832 $user,
16833 array( $site ),
16834 $redirect,
16835 $auto_install,
16836 false
16837 );
16838 }
16839
16840 /**
16841 * Set user and site identities.
16842 *
16843 * @author Vova Feldman (@svovaf)
16844 * @since 2.0.0
16845 *
16846 * @param FS_User $user
16847 * @param FS_Site[] $installs
16848 * @param bool $redirect
16849 * @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.
16850 * @param bool $is_network_level_opt_in
16851 *
16852 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16853 */
16854 function setup_network_account(
16855 FS_User $user,
16856 array $installs,
16857 $redirect = true,
16858 $auto_install = false,
16859 $is_network_level_opt_in = true
16860 ) {
16861 $first_install = $installs[0];
16862
16863 $this->_user = $user;
16864 $this->_site = $first_install;
16865
16866 $this->_sync_plans();
16867
16868 if ( $this->_storage->handle_gdpr_admin_notice &&
16869 $this->should_handle_gdpr_admin_notice() &&
16870 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
16871 ) {
16872 /**
16873 * Clear user lock after an opt-in.
16874 */
16875 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
16876 FS_User_Lock::instance()->unlock();
16877 }
16878
16879 if ( 1 < count( $installs ) ) {
16880 // Only network level opt-in can have more than one install.
16881 $is_network_level_opt_in = true;
16882 }
16883 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
16884 // If Freemius was OFF before, turn it on.
16885 $this->turn_on();
16886
16887 $this->handle_account_connection(
16888 $installs,
16889 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
16890 );
16891
16892 if ( is_numeric( $first_install->license_id ) ) {
16893 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
16894 }
16895
16896 $this->_admin_notices->remove_sticky( 'connect_account' );
16897
16898 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
16899 // Remove pending activation sticky notice (if still exist).
16900 $this->_admin_notices->remove_sticky( 'activation_pending' );
16901
16902 // Remove plugin from pending activation mode.
16903 unset( $this->_storage->is_pending_activation );
16904
16905 if ( ! $this->is_paying_or_trial() ) {
16906 $this->_admin_notices->add_sticky(
16907 sprintf( $this->get_text_inline( '%s activation was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
16908 'activation_complete'
16909 );
16910 }
16911 }
16912
16913 if ( $this->is_paying_or_trial() ) {
16914 if ( ! $this->is_premium() ||
16915 ! $this->has_premium_version() ||
16916 ! $this->has_settings_menu()
16917 ) {
16918 if ( $this->is_paying() ) {
16919 $this->_admin_notices->add_sticky(
16920 sprintf(
16921 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
16922 $this->get_plan_title()
16923 ) . $this->get_complete_upgrade_instructions(),
16924 'plan_upgraded',
16925 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16926 );
16927 } else {
16928 $trial_plan = $this->get_trial_plan();
16929
16930 $this->_admin_notices->add_sticky(
16931 sprintf(
16932 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
16933 '<i>' . $this->get_plugin_name() . '</i>'
16934 ) . $this->get_complete_upgrade_instructions( $trial_plan->title ),
16935 'trial_started',
16936 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16937 );
16938 }
16939 }
16940
16941 $this->_admin_notices->remove_sticky( array(
16942 'trial_promotion',
16943 ) );
16944 }
16945
16946 $plugin_id = fs_request_get( 'plugin_id', false );
16947
16948 // Store activation time ONLY for plugins & themes (not add-ons).
16949 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
16950 if ( empty( $this->_storage->activation_timestamp ) ) {
16951 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
16952 }
16953 }
16954
16955 $next_page = '';
16956
16957 $extra = array();
16958 if ( $auto_install ) {
16959 $extra['auto_install'] = 'true';
16960 }
16961
16962 if ( is_numeric( $plugin_id ) ) {
16963 /**
16964 * @author Leo Fajardo (@leorw)
16965 * @since 1.2.1.6
16966 *
16967 * Also sync the license after an anonymous user subscribes.
16968 */
16969 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
16970 // Add-on was installed - sync license right after install.
16971 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
16972 }
16973 } else {
16974 /**
16975 * @author Vova Feldman (@svovaf)
16976 * @since 1.1.9 If site installed with a valid license, sync license.
16977 */
16978 if ( $this->is_paying() ) {
16979 $this->_sync_plugin_license(
16980 true,
16981 // Installs data is already synced in the beginning of this method directly or via _set_account().
16982 false
16983 );
16984 }
16985
16986 // Reload the page with the keys.
16987 $next_page = $this->is_anonymous() ?
16988 // If user previously skipped, redirect to account page.
16989 $this->get_account_url( false, $extra ) :
16990 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
16991 }
16992
16993 if ( ! empty( $next_page ) && $redirect ) {
16994 fs_redirect( $next_page );
16995 }
16996
16997 return $next_page;
16998 }
16999
17000 /**
17001 * Install plugin with new user information after approval.
17002 *
17003 * @author Vova Feldman (@svovaf)
17004 * @since 1.0.7
17005 */
17006 function _install_with_new_user() {
17007 $this->_logger->entrance();
17008
17009 if ( $this->is_registered() ) {
17010 return;
17011 }
17012
17013 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17014 // @todo This logic should be improved because it's executed on every load of a theme.
17015 $this->is_theme()
17016 ) {
17017 // check_admin_referer( $this->_slug . '_activate_new' );
17018
17019 if ( fs_request_has( 'user_secret_key' ) ) {
17020 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17021 $pending_sites_info = $this->_storage->pending_sites_info;
17022
17023 $this->install_many_pending_with_user(
17024 fs_request_get( 'user_id' ),
17025 fs_request_get( 'user_public_key' ),
17026 fs_request_get( 'user_secret_key' ),
17027 fs_request_get_bool( 'is_marketing_allowed', null ),
17028 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17029 $pending_sites_info['blog_ids'],
17030 $pending_sites_info['license_key'],
17031 $pending_sites_info['trial_plan_id']
17032 );
17033 } else {
17034 $this->install_with_new_user(
17035 fs_request_get( 'user_id' ),
17036 fs_request_get( 'user_public_key' ),
17037 fs_request_get( 'user_secret_key' ),
17038 fs_request_get_bool( 'is_marketing_allowed', null ),
17039 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17040 fs_request_get( 'install_id' ),
17041 fs_request_get( 'install_public_key' ),
17042 fs_request_get( 'install_secret_key' ),
17043 true,
17044 fs_request_get_bool( 'auto_install' )
17045 );
17046 }
17047 } else if ( fs_request_has( 'pending_activation' ) ) {
17048 $this->set_pending_confirmation( fs_request_get( 'user_email' ), true );
17049 }
17050 }
17051 }
17052
17053 /**
17054 * @author Vova Feldman (@svovaf)
17055 * @since 2.0.0
17056 *
17057 * @param number $id
17058 * @param string $public_key
17059 * @param string $secret_key
17060 *
17061 * @return \FS_User
17062 */
17063 private function setup_user( $id, $public_key, $secret_key ) {
17064 $user = self::_get_user_by_id( $id );
17065
17066 if ( is_object( $user ) ) {
17067 $this->_user = $user;
17068 } else {
17069 $user = new FS_User();
17070 $user->id = $id;
17071 $user->public_key = $public_key;
17072 $user->secret_key = $secret_key;
17073
17074 $this->_user = $user;
17075 $user_result = $this->get_api_user_scope()->get();
17076 $user = new FS_User( $user_result );
17077
17078 $this->_user = $user;
17079 $this->_store_user();
17080 }
17081
17082 return $user;
17083 }
17084
17085 /**
17086 * Install plugin with new user.
17087 *
17088 * @author Vova Feldman (@svovaf)
17089 * @since 1.1.7.4
17090 *
17091 * @param number $user_id
17092 * @param string $user_public_key
17093 * @param string $user_secret_key
17094 * @param bool|null $is_marketing_allowed
17095 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17096 * @param number $install_id
17097 * @param string $install_public_key
17098 * @param string $install_secret_key
17099 * @param bool $redirect
17100 * @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.
17101 *
17102 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17103 */
17104 private function install_with_new_user(
17105 $user_id,
17106 $user_public_key,
17107 $user_secret_key,
17108 $is_marketing_allowed,
17109 $is_extensions_tracking_allowed,
17110 $install_id,
17111 $install_public_key,
17112 $install_secret_key,
17113 $redirect = true,
17114 $auto_install = false
17115 ) {
17116 /**
17117 * This method is also executed after opting in with a license key since the
17118 * license can be potentially associated with a different owner.
17119 *
17120 * @since 2.0.0
17121 */
17122 $user = self::_get_user_by_id( $user_id );
17123
17124 if ( ! is_object( $user ) ) {
17125 $user = new FS_User();
17126 $user->id = $user_id;
17127 $user->public_key = $user_public_key;
17128 $user->secret_key = $user_secret_key;
17129
17130 $this->_user = $user;
17131 $user_result = $this->get_api_user_scope()->get();
17132 $user = new FS_User( $user_result );
17133 }
17134
17135 $this->_user = $user;
17136
17137 $site = new FS_Site();
17138 $site->id = $install_id;
17139 $site->public_key = $install_public_key;
17140 $site->secret_key = $install_secret_key;
17141
17142 $this->_site = $site;
17143 $site_result = $this->get_api_site_scope()->get();
17144 $site = new FS_Site( $site_result );
17145 $this->_site = $site;
17146
17147 if ( ! is_null( $is_marketing_allowed ) ) {
17148 $this->disable_opt_in_notice_and_lock_user();
17149 }
17150
17151 if ( ! is_null( $is_extensions_tracking_allowed ) ) {
17152 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17153 }
17154
17155 return $this->setup_account(
17156 $this->_user,
17157 $this->_site,
17158 $redirect,
17159 $auto_install
17160 );
17161 }
17162
17163 /**
17164 * Install plugin with user.
17165 *
17166 * @author Leo Fajardo (@leorw)
17167 * @since 2.0.0
17168 *
17169 * @param number $user_id
17170 * @param string $user_public_key
17171 * @param string $user_secret_key
17172 * @param bool|null $is_marketing_allowed
17173 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17174 * @param array $site_ids
17175 * @param bool $license_key
17176 * @param bool $trial_plan_id
17177 * @param bool $redirect
17178 *
17179 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17180 */
17181 private function install_many_pending_with_user(
17182 $user_id,
17183 $user_public_key,
17184 $user_secret_key,
17185 $is_marketing_allowed,
17186 $is_extensions_tracking_allowed,
17187 $site_ids,
17188 $license_key = false,
17189 $trial_plan_id = false,
17190 $redirect = true
17191 ) {
17192 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17193
17194 if ( ! is_null( $is_marketing_allowed ) ) {
17195 $this->disable_opt_in_notice_and_lock_user();
17196 }
17197
17198 if ( ! is_null( $is_extensions_tracking_allowed ) ) {
17199 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17200 }
17201
17202 $sites = array();
17203 foreach ( $site_ids as $site_id ) {
17204 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17205 }
17206
17207 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17208 }
17209
17210 /**
17211 * Multi-site install with a new user.
17212 *
17213 * @author Vova Feldman (@svovaf)
17214 * @since 2.0.0
17215 *
17216 * @param number $user_id
17217 * @param string $user_public_key
17218 * @param string $user_secret_key
17219 * @param bool|null $is_marketing_allowed
17220 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17221 * @param object[] $installs
17222 * @param bool $redirect
17223 * @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.
17224 *
17225 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17226 */
17227 private function install_many_with_new_user(
17228 $user_id,
17229 $user_public_key,
17230 $user_secret_key,
17231 $is_marketing_allowed,
17232 $is_extensions_tracking_allowed,
17233 array $installs,
17234 $redirect = true,
17235 $auto_install = false
17236 ) {
17237 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17238
17239 if ( ! is_null( $is_marketing_allowed ) ) {
17240 $this->disable_opt_in_notice_and_lock_user();
17241 }
17242
17243 if ( ! is_null( $is_extensions_tracking_allowed ) ) {
17244 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
17245 }
17246
17247 $install_ids = array();
17248
17249 foreach ( $installs as $install ) {
17250 $install_ids[] = $install->id;
17251 }
17252
17253 $left = count( $install_ids );
17254 $offset = 0;
17255
17256 $installs = array();
17257 while ( $left > 0 ) {
17258 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, 25 ) ) );
17259
17260 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17261 // @todo Handle API error.
17262 }
17263
17264 $installs = array_merge( $installs, $result->installs );
17265
17266 $left -= 25;
17267 }
17268
17269 foreach ( $installs as &$install ) {
17270 $install = new FS_Site( $install );
17271 }
17272
17273 return $this->setup_network_account(
17274 $this->_user,
17275 $installs,
17276 $redirect,
17277 $auto_install
17278 );
17279 }
17280
17281 /**
17282 * @author Vova Feldman (@svovaf)
17283 * @since 1.1.7.4
17284 *
17285 * @param string|bool $email
17286 * @param bool $redirect
17287 * @param string|bool $license_key Since 1.2.1.5
17288 * @param bool $is_pending_trial Since 1.2.1.5
17289 *
17290 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17291 */
17292 private function set_pending_confirmation(
17293 $email = false,
17294 $redirect = true,
17295 $license_key = false,
17296 $is_pending_trial = false
17297 ) {
17298 if ( $this->_ignore_pending_mode ) {
17299 /**
17300 * If explicitly asked to ignore pending mode, set to anonymous mode
17301 * if require confirmation before finalizing the opt-in.
17302 *
17303 * @author Vova Feldman
17304 * @since 1.2.1.6
17305 */
17306 $this->skip_connection( null, fs_is_network_admin() );
17307 } else {
17308 // Install must be activated via email since
17309 // user with the same email already exist.
17310 $this->_storage->is_pending_activation = true;
17311 $this->_add_pending_activation_notice( $email, $is_pending_trial );
17312 }
17313
17314 if ( ! empty( $license_key ) ) {
17315 $this->_storage->pending_license_key = $license_key;
17316 }
17317
17318 // Remove the opt-in sticky notice.
17319 $this->_admin_notices->remove_sticky( array(
17320 'connect_account',
17321 'trial_promotion',
17322 ) );
17323
17324 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17325
17326 // Reload the page with with pending activation message.
17327 if ( $redirect ) {
17328 fs_redirect( $next_page );
17329 }
17330
17331 return $next_page;
17332 }
17333
17334 /**
17335 * Install plugin with current logged WP user info.
17336 *
17337 * @author Vova Feldman (@svovaf)
17338 * @since 1.0.7
17339 */
17340 function _install_with_current_user() {
17341 $this->_logger->entrance();
17342
17343 if ( $this->is_registered() ) {
17344 return;
17345 }
17346
17347 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17348 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
17349
17350 /**
17351 * @author Vova Feldman (@svovaf)
17352 * @since 1.1.9 Add license key if given.
17353 */
17354 $license_key = fs_request_get( 'license_secret_key' );
17355
17356 $this->update_extensions_tracking_flag( fs_request_get_bool( 'is_extensions_tracking_allowed', true ) );
17357
17358 $this->install_with_current_user( $license_key );
17359 }
17360 }
17361
17362
17363 /**
17364 * @author Vova Feldman (@svovaf)
17365 * @since 1.1.7.4
17366 *
17367 * @param string|bool $license_key
17368 * @param number|bool $trial_plan_id
17369 * @param array $sites Since 2.0.0
17370 * @param bool $redirect
17371 *
17372 * @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.
17373 */
17374 private function install_with_current_user(
17375 $license_key = false,
17376 $trial_plan_id = false,
17377 $sites = array(),
17378 $redirect = true
17379 ) {
17380 // Get current logged WP user.
17381 $current_user = self::_get_current_wp_user();
17382
17383 // Find the relevant FS user by the email.
17384 $user = self::_get_user_by_email( $current_user->user_email );
17385
17386 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17387 }
17388
17389 /**
17390 * @author Vova Feldman (@svovaf)
17391 * @since 2.0.0
17392 *
17393 * @param \FS_User $user
17394 * @param string|bool $license_key
17395 * @param number|bool $trial_plan_id
17396 * @param bool $redirect
17397 * @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.
17398 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
17399 *
17400 * @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.
17401 */
17402 function install_with_user(
17403 FS_User $user,
17404 $license_key = false,
17405 $trial_plan_id = false,
17406 $redirect = true,
17407 $setup_account = true,
17408 $sites = array()
17409 ) {
17410 // We have to set the user before getting user scope API handler.
17411 $this->_user = $user;
17412
17413 // Install the plugin.
17414 $result = $this->create_installs_with_user(
17415 $user,
17416 $license_key,
17417 $trial_plan_id,
17418 $sites,
17419 $redirect
17420 );
17421
17422 if ( ! $this->is_api_result_entity( $result ) &&
17423 ! $this->is_api_result_object( $result, 'installs' )
17424 ) {
17425 // @todo Handler potential API error of the $result
17426 }
17427
17428 if ( empty( $sites ) ) {
17429 $site = new FS_Site( $result );
17430 $this->_site = $site;
17431
17432 if ( ! $setup_account ) {
17433 $this->_store_site();
17434
17435 $this->sync_plan_if_not_exist( $site->plan_id );
17436
17437 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
17438 $this->sync_license_if_not_exist( $site->license_id, $license_key );
17439 }
17440
17441 $this->_admin_notices->remove_sticky( 'connect_account', false );
17442
17443 return $site;
17444 }
17445
17446 return $this->setup_account( $this->_user, $this->_site, $redirect );
17447 } else {
17448 $installs = array();
17449 foreach ( $result->installs as $install ) {
17450 $installs[] = new FS_Site( $install );
17451 }
17452
17453 return $this->setup_network_account(
17454 $user,
17455 $installs,
17456 $redirect
17457 );
17458 }
17459 }
17460
17461 /**
17462 * Initiate an API request to create a collection of installs.
17463 *
17464 * @author Vova Feldman (@svovaf)
17465 * @since 2.0.0
17466 *
17467 * @param \FS_User $user
17468 * @param bool $license_key
17469 * @param bool $trial_plan_id
17470 * @param array $sites
17471 * @param bool $redirect
17472 * @param bool $silent
17473 *
17474 * @return object|mixed
17475 */
17476 private function create_installs_with_user(
17477 FS_User $user,
17478 $license_key = false,
17479 $trial_plan_id = false,
17480 $sites = array(),
17481 $redirect = false,
17482 $silent = false
17483 ) {
17484 $extra_install_params = array(
17485 'uid' => $this->get_anonymous_id(),
17486 'is_disconnected' => false,
17487 );
17488
17489 if ( ! empty( $license_key ) ) {
17490 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
17491
17492 if ( $silent ) {
17493 $extra_install_params['ignore_license_owner'] = true;
17494 }
17495 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17496 $extra_install_params['trial_plan_id'] = $trial_plan_id;
17497 }
17498
17499 if ( ! empty( $sites ) ) {
17500 $extra_install_params['sites'] = $sites;
17501 }
17502
17503 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
17504
17505 // Install the plugin.
17506 $result = $this->get_api_user_scope_by_user( $user )->call(
17507 "/plugins/{$this->get_id()}/installs.json",
17508 'post',
17509 $args
17510 );
17511
17512 if ( ! $this->is_api_result_entity( $result ) &&
17513 ! $this->is_api_result_object( $result, 'installs' )
17514 ) {
17515 if ( ! empty( $args['license_key'] ) ) {
17516 // Pass the fully entered license key to the failure handler.
17517 $args['license_key'] = $license_key;
17518 }
17519
17520 $result = $this->apply_filters( 'after_install_failure', $result, $args );
17521
17522 if ( ! $silent ) {
17523 $this->_admin_notices->add(
17524 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17525 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
17526 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17527 'error'
17528 );
17529 }
17530
17531 if ( $redirect ) {
17532 /**
17533 * We set the user before getting the user scope API handler, so the user became temporarily
17534 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
17535 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
17536 * activation page based on the return value of `is_registered()`. In addition, in case the
17537 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
17538 * misleading plugin activation errors will be shown on the `Plugins` page.
17539 *
17540 * @author Leo Fajardo (@leorw)
17541 */
17542 $this->_user = null;
17543
17544 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
17545 }
17546 }
17547
17548 return $result;
17549 }
17550
17551 /**
17552 * Tries to activate add-on account based on parent plugin info.
17553 *
17554 * @author Vova Feldman (@svovaf)
17555 * @since 1.0.6
17556 *
17557 * @param Freemius $parent_fs
17558 * @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.
17559 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
17560 */
17561 private function _activate_addon_account(
17562 Freemius $parent_fs,
17563 $network_level_or_blog_id = null,
17564 FS_Plugin_License $bundle_license = null
17565 ) {
17566 if ( $this->is_registered() ) {
17567 // Already activated.
17568 return;
17569 }
17570
17571 /**
17572 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
17573 * already returns the data for the current blog.
17574 *
17575 * @author Leo Fajardo (@leorw)
17576 * @since 2.3.0
17577 */
17578 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
17579 array() :
17580 array( 'uid' => $this->get_anonymous_id() );
17581
17582 $params = $this->get_install_data_for_api(
17583 $uid_param_to_override,
17584 false,
17585 false,
17586 /**
17587 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
17588 * already includes the data for it.
17589 *
17590 * @author Leo Fajardo (@leorw)
17591 * @since 2.3.0
17592 */
17593 ( true !== $network_level_or_blog_id )
17594 );
17595
17596 if ( true === $network_level_or_blog_id ) {
17597 $params['sites'] = $this->get_sites_for_network_level_optin();
17598
17599 if ( empty( $params['sites'] ) ) {
17600 return;
17601 }
17602 }
17603
17604 if ( is_object( $bundle_license ) ) {
17605 $params['license_key'] = $bundle_license->secret_key;
17606 }
17607
17608 // Activate add-on with parent plugin credentials.
17609 $result = $parent_fs->get_api_site_scope()->call(
17610 "/addons/{$this->_plugin->id}/installs.json",
17611 'post',
17612 $params
17613 );
17614
17615 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17616 if ( is_object( $bundle_license ) ) {
17617 /**
17618 * 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.
17619 *
17620 * @author Leo Fajardo (@leorw)
17621 * @since 2.4.0
17622 */
17623 } else {
17624 $error_message = FS_Api::is_api_error_object( $result ) ?
17625 $result->error->message :
17626 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
17627
17628 $this->_admin_notices->add(
17629 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17630 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
17631 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17632 'error'
17633 );
17634 }
17635
17636 return;
17637 }
17638
17639 $addon_installs = $result->installs;
17640 foreach ( $addon_installs as $key => $addon_install ) {
17641 $addon_installs[ $key ] = new FS_Site( $addon_install );
17642 }
17643
17644 $first_install = $addon_installs[0];
17645
17646 // Get user information based on parent's plugin.
17647 $user = $parent_fs->get_user();
17648
17649 // First of all, set site and user info - otherwise we won't
17650 // be able to invoke API calls.
17651 $this->_site = $first_install;
17652 $this->_user = $user;
17653
17654 // Sync add-on plans.
17655 $this->_sync_plans();
17656
17657 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
17658
17659 // Get site's current plan.
17660 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
17661
17662 // Sync licenses.
17663 $this->_sync_licenses();
17664
17665 if ( ! fs_is_network_admin() ) {
17666 // Try to activate premium license.
17667 $this->_activate_license( true, $bundle_license );
17668
17669 if ( is_object( $bundle_license ) ) {
17670 $this->maybe_activate_bundle_license( $bundle_license );
17671 }
17672 } else {
17673 if ( is_object( $bundle_license ) ) {
17674 $premium_license = $bundle_license;
17675 } else {
17676 $license_id = fs_request_get( 'license_id' );
17677
17678 if ( is_object( $this->_site ) &&
17679 FS_Plugin_License::is_valid_id( $license_id ) &&
17680 $license_id == $this->_site->license_id
17681 ) {
17682 // License is already activated.
17683 return;
17684 }
17685
17686 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
17687 $this->_get_license_by_id( $license_id ) :
17688 $this->_get_available_premium_license();
17689 }
17690
17691 if ( is_object( $premium_license ) ) {
17692 $this->maybe_network_activate_addon_license( $premium_license );
17693 }
17694 }
17695 }
17696
17697 /**
17698 * @author Leo Fajardo (@leorw)
17699 * @since 2.3.0
17700 *
17701 * @param FS_Site[] $installs
17702 * @param bool $is_site_level
17703 */
17704 private function handle_account_connection( $installs, $is_site_level ) {
17705 $first_install = $installs[0];
17706
17707 if ( $is_site_level ) {
17708 $this->_set_account( $this->_user, $first_install );
17709
17710 $this->do_action( 'after_account_connection', $this->_user, $first_install );
17711 } else {
17712 $this->_store_user();
17713
17714 // Map site addresses to their blog IDs.
17715 $address_to_blog_map = $this->get_address_to_blog_map();
17716
17717 $first_blog_id = null;
17718 $blog_2_install_map = array();
17719 foreach ( $installs as $install ) {
17720 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
17721 $blog_id = $address_to_blog_map[ $address ];
17722
17723 $this->_store_site( true, $blog_id, $install );
17724
17725 if ( is_null( $first_blog_id ) ) {
17726 $first_blog_id = $blog_id;
17727 }
17728
17729 $blog_2_install_map[ $blog_id ] = $install;
17730 }
17731
17732 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
17733 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
17734 ) {
17735 // Store network user.
17736 $this->_storage->network_user_id = $this->_user->id;
17737 }
17738
17739 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
17740 $this->_storage->network_install_blog_id = $first_blog_id;
17741 }
17742
17743 if ( count( $installs ) === count( $address_to_blog_map ) ) {
17744 // Super admin opted in for all sites in the network.
17745 $this->_storage->is_network_connected = true;
17746 }
17747
17748 $this->_store_licenses( false );
17749
17750 self::$_accounts->store();
17751
17752 // Don't sync the installs data on network upgrade
17753 if ( ! $this->network_upgrade_mode_completed() ) {
17754 $this->send_installs_update();
17755 }
17756
17757 // Switch install context back to the first install.
17758 $this->_site = $first_install;
17759
17760 $current_blog = get_current_blog_id();
17761
17762 foreach ( $blog_2_install_map as $blog_id => $install ) {
17763 $this->switch_to_blog( $blog_id );
17764
17765 $this->do_action( 'after_account_connection', $this->_user, $install );
17766 }
17767
17768 $this->switch_to_blog( $current_blog );
17769
17770 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
17771 }
17772 }
17773
17774 /**
17775 * Tries to activate parent account based on add-on's info.
17776 *
17777 * @author Vova Feldman (@svovaf)
17778 * @since 1.2.2.7
17779 *
17780 * @param Freemius $parent_fs
17781 */
17782 private function activate_parent_account( Freemius $parent_fs ) {
17783 if ( ! $this->is_addon() ) {
17784 // This is not an add-on.
17785 return;
17786 }
17787
17788 if ( $parent_fs->is_registered() ) {
17789 // Already activated.
17790 return;
17791 }
17792
17793 // Activate parent with add-on's user credentials.
17794 $parent_install = $this->get_api_user_scope()->call(
17795 "/plugins/{$parent_fs->_plugin->id}/installs.json",
17796 'post',
17797 $parent_fs->get_install_data_for_api( array(
17798 'uid' => $parent_fs->get_anonymous_id(),
17799 ), false, false )
17800 );
17801
17802 if ( isset( $parent_install->error ) ) {
17803 $this->_admin_notices->add(
17804 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17805 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
17806 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17807 'error'
17808 );
17809
17810 return;
17811 }
17812
17813 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
17814
17815 if ( $parent_fs->is_pending_activation() ) {
17816 $parent_fs->_admin_notices->remove_sticky( 'activation_pending' );
17817
17818 unset( $parent_fs->_storage->is_pending_activation );
17819 }
17820
17821 // Get user information based on parent's plugin.
17822 $user = $this->get_user();
17823
17824 // First of all, set site info - otherwise we won't
17825 // be able to invoke API calls.
17826 $parent_fs->_site = new FS_Site( $parent_install );
17827 $parent_fs->_user = $user;
17828
17829 // Sync add-on plans.
17830 $parent_fs->_sync_plans();
17831
17832 $parent_fs->_set_account( $user, $parent_fs->_site );
17833 }
17834
17835 #endregion
17836
17837 #----------------------------------------------------------------------------------
17838 #region Admin Menu Items
17839 #----------------------------------------------------------------------------------
17840
17841 private $_menu_items = array();
17842
17843 /**
17844 * @author Vova Feldman (@svovaf)
17845 * @since 1.2.1.8
17846 *
17847 * @return array
17848 */
17849 function get_menu_items() {
17850 return $this->_menu_items;
17851 }
17852
17853 /**
17854 * @author Vova Feldman (@svovaf)
17855 * @since 1.0.7
17856 *
17857 * @return string
17858 */
17859 function get_menu_slug() {
17860 return $this->_menu->get_slug();
17861 }
17862
17863 /**
17864 * @author Vova Feldman (@svovaf)
17865 * @since 1.0.9
17866 */
17867 function _prepare_admin_menu() {
17868 // if ( ! $this->is_on() ) {
17869 // return;
17870 // }
17871
17872 /**
17873 * When running from a site admin with a network activated module and the connection
17874 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
17875 * site level settings.
17876 *
17877 * @author Vova Feldman (@svovaf)
17878 * @since 2.0.0
17879 */
17880 $should_hide_site_admin_settings = (
17881 $this->_is_network_active &&
17882 ! fs_is_network_admin() &&
17883 ! $this->is_delegated_connection() &&
17884 ! $this->is_anonymous() &&
17885 ! $this->is_registered()
17886 );
17887
17888 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
17889
17890 if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
17891 $should_hide_site_admin_settings
17892 ) {
17893 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
17894 } else {
17895 $this->do_action( fs_is_network_admin() ?
17896 'before_network_admin_menu_init' :
17897 'before_admin_menu_init'
17898 );
17899
17900 $this->add_menu_action();
17901
17902 $this->add_network_menu_when_missing();
17903
17904 $this->add_submenu_items();
17905 }
17906 }
17907
17908 /**
17909 * Admin dashboard menu items modifications.
17910 *
17911 * NOTE: admin_menu action executed before admin_init.
17912 *
17913 * @author Vova Feldman (@svovaf)
17914 * @since 1.0.7
17915 *
17916 */
17917 private function add_menu_action() {
17918 if ( $this->is_activation_mode() ) {
17919 if ( $this->show_opt_in_on_setting_page() ) {
17920 $this->override_plugin_menu_with_activation();
17921 } else {
17922 /**
17923 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
17924 */
17925 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
17926 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
17927 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
17928 fs_request_get_bool( 'pending_activation' )
17929 ) {
17930 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
17931 }
17932 }
17933 } else {
17934 if ( ! $this->is_registered() ) {
17935 // If not registered try to install user.
17936 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
17937 $this->_install_with_new_user();
17938 }
17939 } else if (
17940 fs_request_is_action( 'sync_user' ) &&
17941 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
17942 ) {
17943 $this->_handle_account_user_sync();
17944 }
17945 }
17946 }
17947
17948 /**
17949 * @author Vova Feldman (@svovaf)
17950 * @since 1.0.1
17951 */
17952 function _redirect_on_clicked_menu_link() {
17953 $this->_logger->entrance();
17954
17955 $page = fs_request_get('page');
17956 $page = is_string($page) ? strtolower($page) : '';
17957
17958 $this->_logger->log( 'page = ' . $page );
17959
17960 foreach ( $this->_menu_items as $priority => $items ) {
17961 foreach ( $items as $item ) {
17962 if ( isset( $item['url'] ) ) {
17963 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
17964 $this->_logger->log( 'Redirecting to ' . $item['url'] );
17965
17966 fs_redirect( $item['url'] );
17967 }
17968 }
17969 }
17970 }
17971 }
17972
17973 /**
17974 * Remove plugin's all admin menu items & pages, and replace with activation page.
17975 *
17976 * @author Vova Feldman (@svovaf)
17977 * @since 1.0.1
17978 */
17979 private function override_plugin_menu_with_activation() {
17980 $this->_logger->entrance();
17981
17982 $hook = false;
17983
17984 if ( ! $this->has_settings_menu() ) {
17985 // Add the opt-in page without a menu item.
17986 $hook = FS_Admin_Menu_Manager::add_subpage(
17987 null,
17988 $this->get_plugin_name(),
17989 $this->get_plugin_name(),
17990 'manage_options',
17991 $this->_slug,
17992 array( &$this, '_connect_page_render' )
17993 );
17994 } else if ( $this->_menu->is_top_level() ) {
17995 if ( $this->_menu->is_override_exact() ) {
17996 // Make sure the current page is matching the activation page.
17997 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
17998 return;
17999 }
18000 }
18001
18002 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18003
18004 if ( false === $hook ) {
18005 // Create new menu item just for the opt-in.
18006 $hook = FS_Admin_Menu_Manager::add_page(
18007 $this->get_plugin_name(),
18008 $this->get_plugin_name(),
18009 'manage_options',
18010 $this->_menu->get_slug(),
18011 array( &$this, '_connect_page_render' )
18012 );
18013 }
18014 } else {
18015 $menus = array( $this->_menu->get_parent_slug() );
18016
18017 if ( $this->_menu->is_override_exact() ) {
18018 // Make sure the current page is matching the activation page.
18019 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18020 return;
18021 }
18022 }
18023
18024 foreach ( $menus as $parent_slug ) {
18025 $hook = $this->_menu->override_submenu_action(
18026 $parent_slug,
18027 $this->_menu->get_raw_slug(),
18028 array( &$this, '_connect_page_render' )
18029 );
18030
18031 if ( false !== $hook ) {
18032 // Found plugin's submenu item.
18033 break;
18034 }
18035 }
18036 }
18037
18038 if ( $this->is_activation_page() ) {
18039 // Clean admin page from distracting content.
18040 self::_clean_admin_content_section();
18041 }
18042
18043 if ( false !== $hook ) {
18044 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18045 $this->_install_with_current_user();
18046 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18047 $this->_install_with_new_user();
18048 }
18049 }
18050 }
18051
18052 /**
18053 * If a plugin was network activated and connected but don't have a network
18054 * level settings, then add an artificial menu item for the Account and other
18055 * Freemius settings.
18056 *
18057 * @author Vova Feldman (@svovaf)
18058 * @since 2.0.0
18059 */
18060 private function add_network_menu_when_missing() {
18061 $this->_logger->entrance();
18062
18063 if ( ! $this->_is_network_active ) {
18064 // Plugin wasn't activated on the network level.
18065 return;
18066 }
18067
18068 if ( ! fs_is_network_admin() ) {
18069 // The context is not the network admin.
18070 return;
18071 }
18072
18073 if ( $this->_menu->has_network_menu() ) {
18074 // Plugin already has a network level menu.
18075 return;
18076 }
18077
18078 if ( $this->is_network_activation_mode() ) {
18079 /**
18080 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18081 * screen is being shown.
18082 *
18083 * @author Leo Fajardo (@leorw)
18084 */
18085 return;
18086 }
18087
18088 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18089 if ( $this->is_network_delegated_connection() ) {
18090 // Super-admin delegated the connection to the site admins.
18091 return;
18092 }
18093 }
18094
18095 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18096
18097 if ( $this->_menu->has_menu() ||
18098 ! $this->is_addon() ||
18099 $this->is_activation_mode()
18100 ) {
18101 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18102 $this->get_plugin_name(),
18103 $this->get_plugin_name(),
18104 'manage_options',
18105 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18106 );
18107 }
18108 } else {
18109 $this->_menu->add_subpage_and_update(
18110 $this->_menu->get_parent_slug(),
18111 $this->get_plugin_name(),
18112 $this->get_plugin_name(),
18113 'manage_options',
18114 $this->_menu->get_slug()
18115 );
18116 }
18117 }
18118
18119 /**
18120 * @author Leo Fajardo (@leorw)
18121 * @since 1.2.1
18122 *
18123 * return string
18124 */
18125 function get_top_level_menu_capability() {
18126 global $menu;
18127
18128 $top_level_menu_slug = $this->get_top_level_menu_slug();
18129
18130 foreach ( $menu as $menu_info ) {
18131 /**
18132 * The second element in the menu info array is the capability/role that has access to the menu and the
18133 * third element is the menu slug.
18134 */
18135 if ( $menu_info[2] === $top_level_menu_slug ) {
18136 return $menu_info[1];
18137 }
18138 }
18139
18140 return 'read';
18141 }
18142
18143 /**
18144 * @author Vova Feldman (@svovaf)
18145 * @since 1.0.0
18146 *
18147 * @return string
18148 */
18149 private function get_top_level_menu_slug() {
18150 return ( $this->is_addon() ?
18151 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18152 $this->_menu->get_top_level_menu_slug() );
18153 }
18154
18155 /**
18156 * @author Vova Feldman (@svovaf)
18157 * @since 1.2.2.7
18158 *
18159 * @return string
18160 */
18161 function get_pricing_cta_label() {
18162 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18163
18164 if ( $this->is_in_trial_promotion() &&
18165 ! $this->is_paying_or_trial()
18166 ) {
18167 // If running a trial promotion, modify the pricing to load the trial.
18168 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18169 } else if ( $this->is_paying() ) {
18170 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18171 }
18172
18173 return $label;
18174 }
18175
18176 /**
18177 * @author Vova Feldman (@svovaf)
18178 * @since 1.2.2.7
18179 *
18180 * @return bool
18181 */
18182 function is_pricing_page_visible() {
18183 return (
18184 // Has at least one paid plan.
18185 $this->has_paid_plan() &&
18186 // Didn't ask to hide the pricing page.
18187 $this->is_page_visible( 'pricing' ) &&
18188 // Don't have a valid active license or has more than one plan.
18189 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18190 );
18191 }
18192
18193 /**
18194 * @author Leo Fajardo (@leorw)
18195 * @since 2.3.0
18196 *
18197 * @param bool $is_activation_mode
18198 *
18199 * @return bool
18200 */
18201 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18202 if ( $this->is_addon() ) {
18203 // No submenu items or action links for add-ons.
18204 return false;
18205 }
18206
18207 if ( $this->show_opt_in_on_themes_page() ) {
18208 if ( ! fs_is_network_admin() ) {
18209 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18210 return true;
18211 }
18212 } else if ( $is_activation_mode ) {
18213 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18214 return false;
18215 }
18216
18217 if ( fs_is_network_admin() ) {
18218 /**
18219 * Add submenu items or action links to network level when plugin was network activated and the super
18220 * admin did NOT delegate the connection of all sites to site admins.
18221 */
18222 return (
18223 $this->_is_network_active &&
18224 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18225 ! $this->is_network_delegated_connection() )
18226 );
18227 }
18228
18229 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18230 }
18231
18232 /**
18233 * Add default Freemius menu items.
18234 *
18235 * @author Vova Feldman (@svovaf)
18236 * @since 1.0.0
18237 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18238 */
18239 private function add_submenu_items() {
18240 $this->_logger->entrance();
18241
18242 $is_activation_mode = $this->is_activation_mode();
18243
18244 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18245
18246 if ( $add_submenu_items ) {
18247 if ( $this->has_affiliate_program() ) {
18248 // Add affiliation page.
18249 $this->add_submenu_item(
18250 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18251 array( &$this, '_affiliation_page_render' ),
18252 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18253 'manage_options',
18254 'affiliation',
18255 'Freemius::_clean_admin_content_section',
18256 WP_FS__DEFAULT_PRIORITY,
18257 $this->is_submenu_item_visible( 'affiliation' )
18258 );
18259 }
18260 }
18261
18262 if ( $add_submenu_items ||
18263 ( $is_activation_mode &&
18264 $this->is_only_premium() &&
18265 $this->is_admin_page( 'account' ) &&
18266 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18267 )
18268 ) {
18269 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18270 $show_account = (
18271 $this->is_submenu_item_visible( 'account' ) &&
18272 /**
18273 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18274 */
18275 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18276 );
18277
18278 // Add user account page.
18279 $this->add_submenu_item(
18280 $this->get_text_inline( 'Account', 'account' ),
18281 array( &$this, '_account_page_render' ),
18282 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18283 'manage_options',
18284 'account',
18285 array( &$this, '_account_page_load' ),
18286 WP_FS__DEFAULT_PRIORITY,
18287 ( $add_submenu_items && $show_account )
18288 );
18289 }
18290 }
18291
18292 if ( $add_submenu_items ) {
18293 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18294 // Add contact page.
18295 $this->add_submenu_item(
18296 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18297 array( &$this, '_contact_page_render' ),
18298 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18299 'manage_options',
18300 'contact',
18301 'Freemius::_clean_admin_content_section',
18302 WP_FS__DEFAULT_PRIORITY,
18303 $this->is_submenu_item_visible( 'contact' )
18304 );
18305 }
18306
18307 if ( $this->has_addons() ) {
18308 $this->add_submenu_item(
18309 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18310 array( &$this, '_addons_page_render' ),
18311 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18312 'manage_options',
18313 'addons',
18314 array( &$this, '_addons_page_load' ),
18315 WP_FS__LOWEST_PRIORITY - 1,
18316 $this->is_submenu_item_visible( 'addons' )
18317 );
18318 }
18319 }
18320
18321 if ( $add_submenu_items ||
18322 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18323 ) {
18324 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18325 $show_pricing = (
18326 $this->is_submenu_item_visible( 'pricing' ) &&
18327 $this->is_pricing_page_visible()
18328 );
18329
18330 $pricing_cta_text = $this->get_pricing_cta_label();
18331 $pricing_class = 'upgrade-mode';
18332 if ( $show_pricing ) {
18333 if ( $this->is_in_trial_promotion() &&
18334 ! $this->is_paying_or_trial()
18335 ) {
18336 // If running a trial promotion, modify the pricing to load the trial.
18337 $pricing_class = 'trial-mode';
18338 } else if ( $this->is_paying() ) {
18339 $pricing_class = '';
18340 }
18341 }
18342
18343 // Add upgrade/pricing page.
18344 $this->add_submenu_item(
18345 $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' ) ),
18346 array( &$this, '_pricing_page_render' ),
18347 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
18348 'manage_options',
18349 'pricing',
18350 'Freemius::_clean_admin_content_section',
18351 WP_FS__LOWEST_PRIORITY,
18352 ( $add_submenu_items && $show_pricing ),
18353 $pricing_class
18354 );
18355 }
18356 }
18357
18358 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
18359 /**
18360 * Add the other menu items if there are any when not in activation mode or license activation is not
18361 * required (license activation is required for registered or anonymous users after activating the
18362 * premium version when the site is not in trial mode or there's no active valid license).
18363 *
18364 * @author Leo Fajardo (@leorw)
18365 * @since 2.2.1
18366 */
18367 if ( 0 < count( $this->_menu_items ) ) {
18368 if ( ! $this->_menu->is_top_level() ) {
18369 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
18370
18371 // Append submenu items right after the plugin's submenu item.
18372 $this->order_sub_submenu_items();
18373 } else {
18374 // Append submenu items.
18375 $this->embed_submenu_items();
18376 }
18377 }
18378 }
18379 }
18380
18381 /**
18382 * Moved the actual submenu item additions to a separated function,
18383 * in order to support sub-submenu items when the plugin's settings
18384 * only have a submenu and not top-level menu item.
18385 *
18386 * @author Vova Feldman (@svovaf)
18387 * @since 1.1.4
18388 */
18389 private function embed_submenu_items() {
18390 $item_template = $this->_menu->is_top_level() ?
18391 '<span class="fs-submenu-item %s %s %s">%s</span>' :
18392 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
18393
18394 $top_level_menu_capability = $this->get_top_level_menu_capability();
18395
18396 ksort( $this->_menu_items );
18397
18398 $is_first_submenu_item = true;
18399
18400 foreach ( $this->_menu_items as $priority => $items ) {
18401 foreach ( $items as $item ) {
18402 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
18403
18404 $menu_item = sprintf(
18405 $item_template,
18406 $this->get_unique_affix(),
18407 $item['menu_slug'],
18408 ! empty( $item['class'] ) ? $item['class'] : '',
18409 $item['menu_title']
18410 );
18411
18412 $top_level_menu_slug = $this->get_top_level_menu_slug();
18413 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
18414
18415 if ( ! isset( $item['url'] ) ) {
18416 $hook = FS_Admin_Menu_Manager::add_subpage(
18417 $item['show_submenu'] ?
18418 $top_level_menu_slug :
18419 null,
18420 $item['page_title'],
18421 $menu_item,
18422 $capability,
18423 $menu_slug,
18424 $item['render_function']
18425 );
18426
18427 if ( false !== $item['before_render_function'] ) {
18428 add_action( "load-$hook", $item['before_render_function'] );
18429 }
18430 } else {
18431 FS_Admin_Menu_Manager::add_subpage(
18432 $item['show_submenu'] ?
18433 $top_level_menu_slug :
18434 null,
18435 $item['page_title'],
18436 $menu_item,
18437 $capability,
18438 $menu_slug,
18439 array( $this, '' )
18440 );
18441 }
18442
18443 if ( $item['show_submenu'] && $is_first_submenu_item ) {
18444 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
18445 /**
18446 * If the top-level menu has been dynamically created, remove the first submenu item that
18447 * WordPress automatically creates when there's no submenu item whose slug matches the
18448 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
18449 *
18450 * Awesome Plugin
18451 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
18452 *
18453 * @author Leo Fajardo (@leorw)
18454 */
18455 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
18456 }
18457
18458 $is_first_submenu_item = false;
18459 }
18460 }
18461 }
18462 }
18463
18464 /**
18465 * Re-order the submenu items so all Freemius added new submenu items
18466 * are added right after the plugin's settings submenu item.
18467 *
18468 * @author Vova Feldman (@svovaf)
18469 * @since 1.1.4
18470 */
18471 private function order_sub_submenu_items() {
18472 global $submenu;
18473
18474 $menu_slug = $this->_menu->get_top_level_menu_slug();
18475
18476 /**
18477 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
18478 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
18479 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
18480 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
18481 *
18482 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
18483 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
18484 * capability needed to access the parent menu as the capability for the submenus that we will add.
18485 */
18486 if ( empty( $submenu[ $menu_slug ] ) ) {
18487 return;
18488 }
18489
18490 $top_level_menu = &$submenu[ $menu_slug ];
18491
18492 $all_submenu_items_after = array();
18493
18494 $found_submenu_item = false;
18495
18496 foreach ( $top_level_menu as $submenu_id => $meta ) {
18497 if ( $found_submenu_item ) {
18498 // Remove all submenu items after the plugin's submenu item.
18499 $all_submenu_items_after[] = $meta;
18500 unset( $top_level_menu[ $submenu_id ] );
18501 }
18502
18503 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
18504 // Found the submenu item, put all below.
18505 $found_submenu_item = true;
18506 continue;
18507 }
18508 }
18509
18510 // Embed all plugin's new submenu items.
18511 $this->embed_submenu_items();
18512
18513 // Start with specially high number to make sure it's appended.
18514 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
18515 foreach ( $all_submenu_items_after as $meta ) {
18516 $top_level_menu[ $i ] = $meta;
18517 $i ++;
18518 }
18519
18520 // Sort submenu items.
18521 ksort( $top_level_menu );
18522 }
18523
18524 /**
18525 * Helper method to return the module's support forum URL.
18526 *
18527 * @author Vova Feldman (@svovaf)
18528 * @since 1.2.2.7
18529 *
18530 * @return string
18531 */
18532 function get_support_forum_url() {
18533 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
18534 }
18535
18536 /**
18537 * Displays the Support Forum link when enabled.
18538 *
18539 * Can be filtered like so:
18540 *
18541 * function _fs_show_support_menu( $is_visible, $menu_id ) {
18542 * if ( 'support' === $menu_id ) {
18543 * return _fs->is_registered();
18544 * }
18545 * return $is_visible;
18546 * }
18547 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
18548 *
18549 */
18550 function _add_default_submenu_items() {
18551 if ( ! $this->is_on() ) {
18552 return;
18553 }
18554
18555 if ( ! $this->is_activation_mode() &&
18556 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
18557 ( ! $this->_is_network_active && is_admin() ) )
18558 ) {
18559 $this->add_submenu_link_item(
18560 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
18561 $this->get_support_forum_url(),
18562 'wp-support-forum',
18563 null,
18564 50,
18565 $this->is_submenu_item_visible( 'support' )
18566 );
18567 }
18568 }
18569
18570 /**
18571 * @author Vova Feldman (@svovaf)
18572 * @since 1.0.1
18573 *
18574 * @param string $menu_title
18575 * @param callable $render_function
18576 * @param bool|string $page_title
18577 * @param string $capability
18578 * @param bool|string $menu_slug
18579 * @param bool|callable $before_render_function
18580 * @param int $priority
18581 * @param bool $show_submenu
18582 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
18583 */
18584 function add_submenu_item(
18585 $menu_title,
18586 $render_function,
18587 $page_title = false,
18588 $capability = 'manage_options',
18589 $menu_slug = false,
18590 $before_render_function = false,
18591 $priority = WP_FS__DEFAULT_PRIORITY,
18592 $show_submenu = true,
18593 $class = ''
18594 ) {
18595 $this->_logger->entrance( 'Title = ' . $menu_title );
18596
18597 if ( $this->is_addon() ) {
18598 $parent_fs = $this->get_parent_instance();
18599
18600 if ( is_object( $parent_fs ) ) {
18601 $parent_fs->add_submenu_item(
18602 $menu_title,
18603 $render_function,
18604 $page_title,
18605 $capability,
18606 $menu_slug,
18607 $before_render_function,
18608 $priority,
18609 $show_submenu,
18610 $class
18611 );
18612
18613 return;
18614 }
18615 }
18616
18617 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
18618 $this->_menu_items[ $priority ] = array();
18619 }
18620
18621 $this->_menu_items[ $priority ][] = array(
18622 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
18623 'menu_title' => $menu_title,
18624 'capability' => $capability,
18625 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
18626 'render_function' => $render_function,
18627 'before_render_function' => $before_render_function,
18628 'show_submenu' => $show_submenu,
18629 'class' => $class,
18630 );
18631 }
18632
18633 /**
18634 * @author Vova Feldman (@svovaf)
18635 * @since 1.0.1
18636 *
18637 * @param string $menu_title
18638 * @param string $url
18639 * @param bool $menu_slug
18640 * @param string $capability
18641 * @param int $priority
18642 * @param bool $show_submenu
18643 */
18644 function add_submenu_link_item(
18645 $menu_title,
18646 $url,
18647 $menu_slug = false,
18648 $capability = 'read',
18649 $priority = WP_FS__DEFAULT_PRIORITY,
18650 $show_submenu = true
18651 ) {
18652 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
18653
18654 if ( $this->is_addon() ) {
18655 $parent_fs = $this->get_parent_instance();
18656
18657 if ( is_object( $parent_fs ) ) {
18658 $parent_fs->add_submenu_link_item(
18659 $menu_title,
18660 $url,
18661 $menu_slug,
18662 $capability,
18663 $priority,
18664 $show_submenu
18665 );
18666
18667 return;
18668 }
18669 }
18670
18671 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
18672 $this->_menu_items[ $priority ] = array();
18673 }
18674
18675 $this->_menu_items[ $priority ][] = array(
18676 'menu_title' => $menu_title,
18677 'capability' => $capability,
18678 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
18679 'url' => $url,
18680 'page_title' => $menu_title,
18681 'render_function' => 'fs_dummy',
18682 'before_render_function' => '',
18683 'show_submenu' => $show_submenu,
18684 );
18685 }
18686
18687 #endregion ------------------------------------------------------------------
18688
18689 #--------------------------------------------------------------------------------
18690 #region Admin Notices
18691 #--------------------------------------------------------------------------------
18692
18693 /**
18694 * @author Vova Feldman (@svovaf)
18695 * @since 2.3.1
18696 *
18697 * @param string|string[] $ids
18698 * @param int|null $network_level_or_blog_id
18699 *
18700 * @uses FS_Admin_Notices::remove_sticky()
18701 */
18702 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
18703 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
18704 }
18705
18706 #endregion
18707
18708 #--------------------------------------------------------------------------------
18709 #region Actions / Hooks / Filters
18710 #--------------------------------------------------------------------------------
18711
18712 /**
18713 * @author Vova Feldman (@svovaf)
18714 * @since 1.1.7
18715 *
18716 * @param string $tag
18717 *
18718 * @return string
18719 */
18720 public function get_action_tag( $tag ) {
18721 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
18722 }
18723
18724 /**
18725 * @author Vova Feldman (@svovaf)
18726 * @since 1.2.1.6
18727 *
18728 * @param string $tag
18729 * @param string $slug
18730 * @param bool $is_plugin
18731 *
18732 * @return string
18733 */
18734 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
18735 $action = "fs_{$tag}";
18736
18737 if ( ! empty( $slug ) ) {
18738 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
18739 }
18740
18741 return $action;
18742 }
18743
18744 /**
18745 * Returns a string that can be used to generate a unique action name,
18746 * option name, HTML element ID, or HTML element class.
18747 *
18748 * @author Leo Fajardo (@leorw)
18749 * @since 1.2.2
18750 *
18751 * @return string
18752 */
18753 public function get_unique_affix() {
18754 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
18755 }
18756
18757 /**
18758 * Returns a string that can be used to generate a unique action name,
18759 * option name, HTML element ID, or HTML element class.
18760 *
18761 * @author Vova Feldman (@svovaf)
18762 * @since 1.2.2.5
18763 *
18764 * @param string $slug
18765 * @param bool $is_plugin
18766 *
18767 * @return string
18768 */
18769 static function get_module_unique_affix( $slug, $is_plugin = true ) {
18770 $affix = $slug;
18771
18772 if ( ! $is_plugin ) {
18773 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
18774 }
18775
18776 return $affix;
18777 }
18778
18779 /**
18780 * @author Vova Feldman (@svovaf)
18781 * @since 1.2.1
18782 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
18783 * based on the slug for backward compatibility.
18784 *
18785 * @param string $tag
18786 *
18787 * @return string
18788 */
18789 function get_ajax_action( $tag ) {
18790 return self::get_ajax_action_static( $tag, $this->_module_id );
18791 }
18792
18793 /**
18794 * @author Vova Feldman (@svovaf)
18795 * @since 1.2.1.7
18796 *
18797 * @param string $tag
18798 *
18799 * @return string
18800 */
18801 function get_ajax_security( $tag ) {
18802 return wp_create_nonce( $this->get_ajax_action( $tag ) );
18803 }
18804
18805 /**
18806 * @author Vova Feldman (@svovaf)
18807 * @since 1.2.1.7
18808 *
18809 * @param string $tag
18810 */
18811 function check_ajax_referer( $tag ) {
18812 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
18813 }
18814
18815 /**
18816 * @author Vova Feldman (@svovaf)
18817 * @since 1.2.1.6
18818 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
18819 * based on the slug for backward compatibility.
18820 *
18821 * @param string $tag
18822 * @param number|null $module_id
18823 *
18824 * @return string
18825 */
18826 private static function get_ajax_action_static( $tag, $module_id = null ) {
18827 $action = "fs_{$tag}";
18828
18829 if ( ! empty( $module_id ) ) {
18830 $action .= "_{$module_id}";
18831 }
18832
18833 return $action;
18834 }
18835
18836 /**
18837 * Do action, specific for the current context plugin.
18838 *
18839 * @author Vova Feldman (@svovaf)
18840 * @since 1.0.1
18841 *
18842 * @param string $tag The name of the action to be executed.
18843 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
18844 * functions hooked to the action. Default empty.
18845 *
18846 * @uses do_action()
18847 */
18848 function do_action( $tag, $arg = '' ) {
18849 $this->_logger->entrance( $tag );
18850
18851 $args = func_get_args();
18852
18853 call_user_func_array( 'do_action', array_merge(
18854 array( $this->get_action_tag( $tag ) ),
18855 array_slice( $args, 1 ) )
18856 );
18857 }
18858
18859 /**
18860 * Add action, specific for the current context plugin.
18861 *
18862 * @author Vova Feldman (@svovaf)
18863 * @since 1.0.1
18864 *
18865 * @param string $tag
18866 * @param callable $function_to_add
18867 * @param int $priority
18868 * @param int $accepted_args
18869 *
18870 * @uses add_action()
18871 */
18872 function add_action(
18873 $tag,
18874 $function_to_add,
18875 $priority = WP_FS__DEFAULT_PRIORITY,
18876 $accepted_args = 1
18877 ) {
18878 $this->_logger->entrance( $tag );
18879
18880 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
18881 }
18882
18883 /**
18884 * Add AJAX action, specific for the current context plugin.
18885 *
18886 * @author Vova Feldman (@svovaf)
18887 * @since 1.2.1
18888 *
18889 * @param string $tag
18890 * @param callable $function_to_add
18891 * @param int $priority
18892 *
18893 * @uses add_action()
18894 *
18895 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
18896 */
18897 function add_ajax_action(
18898 $tag,
18899 $function_to_add,
18900 $priority = WP_FS__DEFAULT_PRIORITY
18901 ) {
18902 $this->_logger->entrance( $tag );
18903
18904 return self::add_ajax_action_static(
18905 $tag,
18906 $function_to_add,
18907 $priority,
18908 $this->_module_id
18909 );
18910 }
18911
18912 /**
18913 * Add AJAX action.
18914 *
18915 * @author Vova Feldman (@svovaf)
18916 * @since 1.2.1.6
18917 *
18918 * @param string $tag
18919 * @param callable $function_to_add
18920 * @param int $priority
18921 * @param number|null $module_id
18922 *
18923 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
18924 * @uses add_action()
18925 *
18926 */
18927 static function add_ajax_action_static(
18928 $tag,
18929 $function_to_add,
18930 $priority = WP_FS__DEFAULT_PRIORITY,
18931 $module_id = null
18932 ) {
18933 self::$_static_logger->entrance( $tag );
18934
18935 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
18936 return false;
18937 }
18938
18939 add_action(
18940 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
18941 $function_to_add,
18942 $priority,
18943 0
18944 );
18945
18946 self::$_static_logger->info( "$tag AJAX callback action added." );
18947
18948 return true;
18949 }
18950
18951 /**
18952 * Send a JSON response back to an Ajax request.
18953 *
18954 * @author Vova Feldman (@svovaf)
18955 * @since 1.2.1.5
18956 *
18957 * @param mixed $response
18958 */
18959 static function shoot_ajax_response( $response ) {
18960 wp_send_json( $response );
18961 }
18962
18963 /**
18964 * Send a JSON response back to an Ajax request, indicating success.
18965 *
18966 * @author Vova Feldman (@svovaf)
18967 * @since 1.2.1.5
18968 *
18969 * @param mixed $data Data to encode as JSON, then print and exit.
18970 */
18971 static function shoot_ajax_success( $data = null ) {
18972 wp_send_json_success( $data );
18973 }
18974
18975 /**
18976 * Send a JSON response back to an Ajax request, indicating failure.
18977 *
18978 * @author Vova Feldman (@svovaf)
18979 * @since 1.2.1.5
18980 *
18981 * @param mixed $error Optional error message.
18982 */
18983 static function shoot_ajax_failure( $error = '' ) {
18984 $result = array( 'success' => false );
18985 if ( ! empty( $error ) ) {
18986 $result['error'] = $error;
18987 }
18988
18989 wp_send_json( $result );
18990 }
18991
18992 /**
18993 * Apply filter, specific for the current context plugin.
18994 *
18995 * @author Vova Feldman (@svovaf)
18996 * @since 1.0.9
18997 *
18998 * @param string $tag The name of the filter hook.
18999 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19000 *
19001 * @return mixed The filtered value after all hooked functions are applied to it.
19002 *
19003 * @uses apply_filters()
19004 */
19005 function apply_filters( $tag, $value ) {
19006 $this->_logger->entrance( $tag );
19007
19008 $args = func_get_args();
19009 array_unshift( $args, $this->get_unique_affix() );
19010
19011 return call_user_func_array( 'fs_apply_filter', $args );
19012 }
19013
19014 /**
19015 * Add filter, specific for the current context plugin.
19016 *
19017 * @author Vova Feldman (@svovaf)
19018 * @since 1.0.9
19019 *
19020 * @param string $tag
19021 * @param callable $function_to_add
19022 * @param int $priority
19023 * @param int $accepted_args
19024 *
19025 * @uses add_filter()
19026 */
19027 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19028 $this->_logger->entrance( $tag );
19029
19030 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19031 }
19032
19033 /**
19034 * Check if has filter.
19035 *
19036 * @author Vova Feldman (@svovaf)
19037 * @since 1.1.4
19038 *
19039 * @param string $tag
19040 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19041 *
19042 * @return false|int
19043 *
19044 * @uses has_filter()
19045 */
19046 function has_filter( $tag, $function_to_check = false ) {
19047 $this->_logger->entrance( $tag );
19048
19049 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19050 }
19051
19052 #endregion
19053
19054 /**
19055 * Override default i18n text phrases.
19056 *
19057 * @author Vova Feldman (@svovaf)
19058 * @since 1.1.6
19059 *
19060 * @param string[] string $key_value
19061 *
19062 * @uses fs_override_i18n()
19063 */
19064 function override_i18n( $key_value ) {
19065 fs_override_i18n( $key_value, $this->_slug );
19066 }
19067
19068 /* Account Page
19069 ------------------------------------------------------------------------------------------------------------------*/
19070 /**
19071 * Update site information.
19072 *
19073 * @author Vova Feldman (@svovaf)
19074 * @since 1.0.1
19075 *
19076 * @param bool $store Flush to Database if true.
19077 * @param null|int $network_level_or_blog_id Since 2.0.0
19078 * @param \FS_Site $site Since 2.0.0
19079 */
19080 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null ) {
19081 $this->_logger->entrance();
19082
19083 if ( is_null( $site ) ) {
19084 $site = $this->_site;
19085 }
19086
19087 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19088 $this->_logger->error( "Empty install ID, can't store site." );
19089
19090 return;
19091 }
19092
19093 $site_clone = clone $site;
19094
19095 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id );
19096
19097 if ( is_object( $this->_user ) && $this->_user->id != $site->user_id ) {
19098 $this->sync_user_by_current_install( $site->user_id );
19099
19100 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19101
19102 if ( empty( $prev_stored_user_id ) &&
19103 is_object($this->_user) && $this->_user->id != $site->user_id
19104 ) {
19105 /**
19106 * Store the current user ID as the previous user ID so that the previous user can be used
19107 * as the install's owner while the new owner's details are not yet available.
19108 *
19109 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19110 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19111 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19112 * into `replica`.
19113 *
19114 * @author Leo Fajardo (@leorw)
19115 */
19116 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19117 }
19118 }
19119
19120 $sites[ $this->_slug ] = $site_clone;
19121
19122 $this->set_account_option( 'sites', $sites, $store, $network_level_or_blog_id );
19123 }
19124
19125 /**
19126 * Update plugin's plans information.
19127 *
19128 * @author Vova Feldman (@svovaf)
19129 * @since 1.0.2
19130 *
19131 * @param bool $store Flush to Database if true.
19132 */
19133 private function _store_plans( $store = true ) {
19134 $this->_logger->entrance();
19135
19136 $plans = self::get_all_plans( $this->_module_type );
19137
19138 // Copy plans.
19139 $encrypted_plans = array();
19140 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19141 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19142 }
19143
19144 $plans[ $this->_slug ] = $encrypted_plans;
19145
19146 $this->set_account_option( 'plans', $plans, $store );
19147 }
19148
19149 /**
19150 * Update user's plugin licenses.
19151 *
19152 * @author Vova Feldman (@svovaf)
19153 * @since 1.0.5
19154 *
19155 * @param bool $store
19156 * @param number|bool $module_id
19157 * @param FS_Plugin_License[] $licenses
19158 */
19159 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19160 $this->_logger->entrance();
19161
19162 $all_licenses = self::get_all_licenses();
19163
19164 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19165 $module_id = $this->_module_id;
19166
19167 $user_licenses = is_array( $this->_licenses ) ?
19168 $this->_licenses :
19169 array();
19170
19171 if ( empty( $user_licenses ) ) {
19172 // If the context user doesn't have any license, don't update the licenses collection.
19173 return;
19174 }
19175
19176 $new_user_licenses_map = array();
19177 foreach ( $user_licenses as $user_license ) {
19178 $new_user_licenses_map[ $user_license->id ] = $user_license;
19179 }
19180
19181 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19182
19183 // Update user licenses.
19184 $licenses_to_update_count = count( $new_user_licenses_map );
19185 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19186 if ( 0 === $licenses_to_update_count ) {
19187 break;
19188 }
19189
19190 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19191 // Update license.
19192 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19193 unset( $new_user_licenses_map[ $license->id ] );
19194
19195 $licenses_to_update_count --;
19196 }
19197 }
19198
19199 if ( ! empty( $new_user_licenses_map ) ) {
19200 // Add new licenses.
19201 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19202 }
19203
19204 $licenses = $all_licenses[ $module_id ];
19205 }
19206
19207 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19208 $all_licenses[ $module_id ] = array();
19209 }
19210
19211 $all_licenses[ $module_id ] = $licenses;
19212
19213 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19214 }
19215
19216 /**
19217 * Update user information.
19218 *
19219 * @author Vova Feldman (@svovaf)
19220 * @since 1.0.1
19221 *
19222 * @param bool $store Flush to Database if true.
19223 */
19224 private function _store_user( $store = true ) {
19225 $this->_logger->entrance();
19226
19227 if ( empty( $this->_user->id ) ) {
19228 $this->_logger->error( "Empty user ID, can't store user." );
19229
19230 return;
19231 }
19232
19233 $users = self::get_all_users();
19234 $users[ $this->_user->id ] = $this->_user;
19235 self::$_accounts->set_option( 'users', $users, $store );
19236 }
19237
19238 /**
19239 * Update new updates information.
19240 *
19241 * @author Vova Feldman (@svovaf)
19242 * @since 1.0.4
19243 *
19244 * @param FS_Plugin_Tag|null $update
19245 * @param bool $store Flush to Database if true.
19246 * @param bool|number $plugin_id
19247 */
19248 private function _store_update( $update, $store = true, $plugin_id = false ) {
19249 $this->_logger->entrance();
19250
19251 if ( $update instanceof FS_Plugin_Tag ) {
19252 $update->updated = time();
19253 }
19254
19255 if ( ! is_numeric( $plugin_id ) ) {
19256 $plugin_id = $this->_plugin->id;
19257 }
19258
19259 $updates = self::get_all_updates();
19260 $updates[ $plugin_id ] = $update;
19261 self::$_accounts->set_option( 'updates', $updates, $store );
19262 }
19263
19264 /**
19265 * Update new updates information.
19266 *
19267 * @author Vova Feldman (@svovaf)
19268 * @since 1.0.6
19269 *
19270 * @param FS_Plugin[] $plugin_addons
19271 * @param bool $store Flush to Database if true.
19272 */
19273 private function _store_addons( $plugin_addons, $store = true ) {
19274 $this->_logger->entrance();
19275
19276 $addons = self::get_all_addons();
19277 $addons[ $this->_plugin->id ] = $plugin_addons;
19278 self::$_accounts->set_option( 'addons', $addons, $store );
19279 }
19280
19281 /**
19282 * Delete plugin's associated add-ons.
19283 *
19284 * @author Vova Feldman (@svovaf)
19285 * @since 1.0.8
19286 *
19287 * @param bool $store
19288 *
19289 * @return bool
19290 */
19291 private function _delete_account_addons( $store = true ) {
19292 $all_addons = self::get_all_account_addons();
19293
19294 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
19295 return false;
19296 }
19297
19298 unset( $all_addons[ $this->_plugin->id ] );
19299
19300 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19301
19302 return true;
19303 }
19304
19305 /**
19306 * Update account add-ons list.
19307 *
19308 * @author Vova Feldman (@svovaf)
19309 * @since 1.0.6
19310 *
19311 * @param FS_Plugin[] $addons
19312 * @param bool $store Flush to Database if true.
19313 */
19314 private function _store_account_addons( $addons, $store = true ) {
19315 $this->_logger->entrance();
19316
19317 $all_addons = self::get_all_account_addons();
19318 $all_addons[ $this->_plugin->id ] = $addons;
19319 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19320 }
19321
19322 /**
19323 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
19324 * the valid user licenses will be fetched again and the account add-ons may be updated.
19325 *
19326 * @author Leo Fajardo (@leorw)
19327 * @since 2.2.4
19328 */
19329 private function purge_valid_user_licenses_cache() {
19330 if ( ! $this->is_registered() ) {
19331 return;
19332 }
19333
19334 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
19335 }
19336
19337 /**
19338 * @author Leo Fajardo (@leorw)
19339 * @since 2.3.0
19340 *
19341 * @param array $all_licenses
19342 * @param number|null $site_license_id
19343 * @param bool $include_parent_licenses
19344 *
19345 * @return array
19346 */
19347 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
19348 $foreign_licenses = array(
19349 'ids' => array(),
19350 'license_keys' => array()
19351 );
19352
19353 $parent_license_ids_map = array();
19354
19355 foreach ( $all_licenses as $license ) {
19356 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
19357 continue;
19358 }
19359
19360 $foreign_licenses['ids'][] = $license->id;
19361 $foreign_licenses['license_keys'][] = $license->secret_key;
19362
19363 if (
19364 $include_parent_licenses &&
19365 is_object( $this->_license ) &&
19366 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
19367 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
19368 ) {
19369 /**
19370 * Include the parent license's info only if it has not been included before since child licenses
19371 * can have the same parent license.
19372 */
19373 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
19374 $foreign_licenses['license_keys'][] = $license->secret_key;
19375
19376 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
19377 }
19378 }
19379
19380 if ( empty( $foreign_licenses['ids'] ) ) {
19381 $foreign_licenses = array();
19382 }
19383
19384 return $foreign_licenses;
19385 }
19386
19387 /**
19388 * @author Leo Fajardo (@leorw)
19389 * @since 2.3.0
19390 *
19391 * @return string
19392 */
19393 private function get_valid_user_licenses_endpoint() {
19394 $user_licenses_endpoint = '/licenses.json?type=active' .
19395 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
19396
19397 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
19398
19399 if ( ! empty ( $foreign_licenses ) ) {
19400 $foreign_licenses = array(
19401 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
19402 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
19403 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
19404 );
19405
19406 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
19407 }
19408
19409 return $user_licenses_endpoint;
19410 }
19411
19412 /**
19413 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
19414 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
19415 * method filters out non–add-on product IDs and stores the add-on IDs.
19416 *
19417 * @author Leo Fajardo (@leorw)
19418 * @since 2.2.4
19419 *
19420 * @return stdClass[] array
19421 */
19422 private function fetch_valid_user_licenses() {
19423 $this->_logger->entrance();
19424
19425 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
19426
19427 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
19428 ! is_array( $result->licenses )
19429 ) {
19430 return array();
19431 }
19432
19433 return $result->licenses;
19434 }
19435
19436 /**
19437 * @author Leo Fajardo (@leorw)
19438 * @since 2.2.4
19439 *
19440 * @return number[] Account add-on IDs.
19441 */
19442 function get_updated_account_addons() {
19443 $addons = $this->get_addons();
19444 if ( empty( $addons ) ) {
19445 return array();
19446 }
19447
19448 $account_addons = $this->get_account_addons();
19449 if ( ! is_array( $account_addons ) ) {
19450 $account_addons = array();
19451 }
19452
19453 $user_licenses = $this->is_registered() ?
19454 $this->fetch_valid_user_licenses() :
19455 array();
19456
19457 if ( empty( $user_licenses ) ) {
19458 return $account_addons;
19459 }
19460
19461 $addon_ids = array();
19462 foreach ( $addons as $addon ) {
19463 $addon_ids[] = $addon->id;
19464 }
19465
19466 $license_product_ids = array();
19467
19468 foreach ( $user_licenses as $license ) {
19469 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
19470 $license_product_ids = array_merge( $license_product_ids, $license->products );
19471 } else {
19472 $license_product_ids[] = $license->plugin_id;
19473 }
19474 }
19475
19476 // Filter out non–add-on IDs.
19477 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
19478 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
19479 $this->_store_account_addons( array_unique( $new_account_addons ) );
19480 }
19481
19482 return $new_account_addons;
19483 }
19484
19485 /**
19486 * Store account params in the Database.
19487 *
19488 * @author Vova Feldman (@svovaf)
19489 * @since 1.0.1
19490 *
19491 * @param null|int $blog_id Since 2.0.0
19492 */
19493 private function _store_account( $blog_id = null ) {
19494 $this->_logger->entrance();
19495
19496 $this->_store_site( false, $blog_id );
19497 $this->_store_user( false );
19498 $this->_store_plans( false );
19499 $this->_store_licenses( false );
19500
19501 self::$_accounts->store( $blog_id );
19502 }
19503
19504 /**
19505 * Sync user's information.
19506 *
19507 * @author Vova Feldman (@svovaf)
19508 * @since 1.0.3
19509 * @uses FS_Api
19510 */
19511 private function _handle_account_user_sync() {
19512 $this->_logger->entrance();
19513
19514 $api = $this->get_api_user_scope();
19515
19516 // Get user's information.
19517 $user = $api->get( '/', true );
19518
19519 if ( isset( $user->id ) ) {
19520 $this->_user->first = $user->first;
19521 $this->_user->last = $user->last;
19522 $this->_user->email = $user->email;
19523
19524 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
19525
19526 if ( $user->is_verified &&
19527 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
19528 ) {
19529 $this->_user->is_verified = true;
19530
19531 $this->do_action( 'account_email_verified', $user->email );
19532
19533 $this->_admin_notices->add(
19534 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
19535 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
19536 'success',
19537 // Make admin sticky if account menu item is invisible,
19538 // since the page will be auto redirected to the plugin's
19539 // main settings page, and the non-sticky message
19540 // will disappear.
19541 ! $is_menu_item_account_visible,
19542 'email_verified'
19543 );
19544 }
19545
19546 // Flush user details to DB.
19547 $this->_store_user();
19548
19549 $this->do_action( 'after_account_user_sync', $user );
19550
19551 /**
19552 * If account menu item is hidden, redirect to plugin's main settings page.
19553 *
19554 * @author Vova Feldman (@svovaf)
19555 * @since 1.1.6
19556 *
19557 * @link https://github.com/Freemius/wordpress-sdk/issues/6
19558 */
19559 if ( ! $is_menu_item_account_visible ) {
19560 fs_redirect( $this->_get_admin_page_url() );
19561 }
19562 }
19563 }
19564
19565 /**
19566 * @author Vova Feldman (@svovaf)
19567 * @since 1.0.9
19568 * @uses FS_Api
19569 *
19570 * @param number|bool $license_id
19571 *
19572 * @return FS_Subscription|object|bool
19573 */
19574 private function _fetch_site_license_subscription( $license_id = false ) {
19575 $this->_logger->entrance();
19576 $api = $this->get_api_site_scope();
19577
19578 if ( ! is_numeric( $license_id ) ) {
19579 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
19580 $this->_license->parent_license_id :
19581 $this->_license->id;
19582 }
19583
19584 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
19585
19586 return ! isset( $result->error ) ?
19587 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
19588 new FS_Subscription( $result->subscriptions[0] ) :
19589 false
19590 ) :
19591 $result;
19592 }
19593
19594 /**
19595 * @author Vova Feldman (@svovaf)
19596 * @since 1.0.4
19597 * @uses FS_Api
19598 *
19599 * @param number|bool $plan_id
19600 *
19601 * @return FS_Plugin_Plan|object
19602 */
19603 private function _fetch_site_plan( $plan_id = false ) {
19604 $this->_logger->entrance();
19605 $api = $this->get_api_site_scope();
19606
19607 if ( ! is_numeric( $plan_id ) ) {
19608 $plan_id = $this->_site->plan_id;
19609 }
19610
19611 $plan = $api->get( "/plans/{$plan_id}.json", true );
19612
19613 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
19614 }
19615
19616 /**
19617 * @author Vova Feldman (@svovaf)
19618 * @since 1.0.5
19619 * @uses FS_Api
19620 *
19621 * @return FS_Plugin_Plan[]|object
19622 */
19623 private function _fetch_plugin_plans() {
19624 $this->_logger->entrance();
19625 $api = $this->get_current_or_network_user_api_scope();
19626
19627 /**
19628 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
19629 */
19630 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
19631
19632 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
19633 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
19634 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
19635 }
19636
19637 $result = $result->plans;
19638 }
19639
19640 return $result;
19641 }
19642
19643 /**
19644 * @author Vova Feldman (@svovaf)
19645 * @since 2.0.0
19646 *
19647 * @param number $plan_id
19648 *
19649 * @return \FS_Plugin_Plan|object
19650 */
19651 private function fetch_plan_by_id( $plan_id ) {
19652 $this->_logger->entrance();
19653 $api = $this->get_current_or_network_user_api_scope();
19654
19655 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
19656
19657 return $this->is_api_result_entity( $result ) ?
19658 new FS_Plugin_Plan( $result ) :
19659 $result;
19660 }
19661
19662 /**
19663 * @author Vova Feldman (@svovaf)
19664 * @since 1.0.5
19665 * @uses FS_Api
19666 *
19667 * @param number|bool $plugin_id
19668 * @param number|bool $site_license_id
19669 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
19670 * @param number|null $blog_id
19671 *
19672 * @return FS_Plugin_License[]|object
19673 */
19674 private function _fetch_licenses(
19675 $plugin_id = false,
19676 $site_license_id = false,
19677 $foreign_licenses = array(),
19678 $blog_id = null
19679 ) {
19680 $this->_logger->entrance();
19681
19682 $api = $this->get_api_user_scope();
19683
19684 if ( ! is_numeric( $plugin_id ) ) {
19685 $plugin_id = $this->_plugin->id;
19686 }
19687
19688 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
19689 if ( ! empty ( $foreign_licenses ) ) {
19690 $foreign_licenses = array(
19691 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
19692 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
19693 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
19694 );
19695
19696 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
19697 }
19698
19699 $result = $api->get( $user_licenses_endpoint, true );
19700
19701 $is_site_license_synced = false;
19702
19703 $api_errors = array();
19704
19705 if ( $this->is_api_result_object( $result, 'licenses' ) &&
19706 is_array( $result->licenses )
19707 ) {
19708 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
19709 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
19710
19711 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
19712 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
19713 }
19714 }
19715
19716 $result = $result->licenses;
19717 } else {
19718 $api_errors[] = $result;
19719 $result = array();
19720 }
19721
19722 if ( ! $is_site_license_synced ) {
19723 if ( ! is_null( $blog_id ) ) {
19724 /**
19725 * If blog ID is not null, the request is for syncing of the license of a single site via the
19726 * network-level "Account" page.
19727 *
19728 * @author Leo Fajardo (@leorw)
19729 */
19730 $this->switch_to_blog( $blog_id );
19731 }
19732
19733 $api = $this->get_api_site_scope();
19734
19735 if ( is_numeric( $site_license_id ) ) {
19736 // Try to retrieve a foreign license that is linked to the install.
19737 $api_result = $api->call( '/licenses.json?is_enriched=true' );
19738
19739 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
19740 is_array( $api_result->licenses )
19741 ) {
19742 $licenses = $api_result->licenses;
19743
19744 if ( ! empty( $licenses ) ) {
19745 $result[] = new FS_Plugin_License( $licenses[0] );
19746 }
19747 } else {
19748 $api_errors[] = $api_result;
19749 }
19750 } else if (
19751 is_object( $this->_license ) &&
19752 /**
19753 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
19754 * the FS instance that does the syncing is the parent FS instance.
19755 *
19756 * @author Leo Fajardo (@leorw)
19757 * @since 2.3.0
19758 */
19759 $this->_license->plugin_id == $plugin_id
19760 ) {
19761 $is_license_in_result = false;
19762 if ( ! empty( $result ) ) {
19763 foreach ( $result as $license ) {
19764 if ( $license->id == $this->_license->id ) {
19765 $is_license_in_result = true;
19766 break;
19767 }
19768 }
19769 }
19770
19771 if ( ! $is_license_in_result ) {
19772 // Fetch foreign license by ID and license key.
19773 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
19774 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
19775
19776 if ( $this->is_api_result_entity( $license ) ) {
19777 $result[] = new FS_Plugin_License( $license );
19778 } else {
19779 $api_errors[] = $license;
19780 }
19781 }
19782 }
19783
19784 if ( ! is_null( $blog_id ) ) {
19785 $this->switch_to_blog( $this->_storage->network_install_blog_id );
19786 }
19787 }
19788
19789 if ( is_array( $result ) && 0 < count( $result ) ) {
19790 // If found at least one license, return license collection even if there are errors.
19791 return $result;
19792 }
19793
19794 if ( ! empty( $api_errors ) ) {
19795 // If found any errors and no licenses, return first error.
19796 return $api_errors[0];
19797 }
19798
19799 // Fallback to empty licenses list.
19800 return $result;
19801 }
19802
19803 /**
19804 * @author Vova Feldman (@svovaf)
19805 * @since 2.0.0
19806 *
19807 * @param number $license_id
19808 * @param string $license_key
19809 *
19810 * @return \FS_Plugin_License|object
19811 */
19812 private function fetch_license_by_key( $license_id, $license_key ) {
19813 $this->_logger->entrance();
19814
19815 $api = $this->get_current_or_network_user_api_scope();
19816
19817 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
19818
19819 return $this->is_api_result_entity( $result ) ?
19820 new FS_Plugin_License( $result ) :
19821 $result;
19822 }
19823
19824 /**
19825 * @author Vova Feldman (@svovaf)
19826 * @since 1.2.0
19827 * @uses FS_Api
19828 *
19829 * @param number|bool $plugin_id
19830 * @param bool $flush
19831 *
19832 * @return FS_Payment[]|object
19833 */
19834 function _fetch_payments( $plugin_id = false, $flush = false ) {
19835 $this->_logger->entrance();
19836
19837 $api = $this->get_api_user_scope();
19838
19839 if ( ! is_numeric( $plugin_id ) ) {
19840 $plugin_id = $this->_plugin->id;
19841 }
19842
19843 $include_bundles = (
19844 is_object( $this->_plugin ) &&
19845 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
19846 );
19847
19848 $result = $api->get(
19849 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
19850 $flush
19851 );
19852
19853 if ( ! isset( $result->error ) ) {
19854 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
19855 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
19856 }
19857 $result = $result->payments;
19858 }
19859
19860 return $result;
19861 }
19862
19863 /**
19864 * @author Vova Feldman (@svovaf)
19865 * @since 1.2.1.5
19866 * @uses FS_Api
19867 *
19868 * @param bool $flush
19869 *
19870 * @return \FS_Billing|mixed
19871 */
19872 function _fetch_billing( $flush = false ) {
19873 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
19874
19875 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
19876
19877 if ( $this->is_api_result_entity( $billing ) ) {
19878 $billing = new FS_Billing( $billing );
19879 }
19880
19881 return $billing;
19882 }
19883
19884 /**
19885 * @author Vova Feldman (@svovaf)
19886 * @since 1.0.5
19887 *
19888 * @param FS_Plugin_License[] $licenses
19889 * @param number $module_id
19890 */
19891 private function _update_licenses( $licenses, $module_id ) {
19892 $this->_logger->entrance();
19893
19894 if ( is_array( $licenses ) ) {
19895 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
19896 $licenses[ $i ]->updated = time();
19897 }
19898 }
19899
19900 $this->_store_licenses( true, $module_id, $licenses );
19901 }
19902
19903 /**
19904 * @author Vova Feldman (@svovaf)
19905 * @since 1.0.4
19906 *
19907 * @param bool|number $plugin_id
19908 * @param bool $flush Since 1.1.7.3
19909 * @param int $expiration Since 1.2.2.7
19910 * @param bool|string $newer_than Since 2.2.1
19911 *
19912 * @return object|false New plugin tag info if exist.
19913 */
19914 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
19915 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
19916
19917 if ( ! is_object( $latest_tag ) ) {
19918 return false;
19919 }
19920
19921 $plugin_version = $this->get_plugin_version();
19922
19923 // Check if version is actually newer.
19924 $has_new_version =
19925 // If it's an non-installed add-on then always return latest.
19926 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
19927 // Compare versions.
19928 version_compare( $plugin_version, $latest_tag->version, '<' );
19929
19930 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
19931
19932 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
19933
19934 $this->_storage->beta_data = array(
19935 'is_beta' => $is_latest_version_beta,
19936 'version' => $latest_tag->version
19937 );
19938
19939 return $has_new_version ? $latest_tag : false;
19940 }
19941
19942 /**
19943 * @author Vova Feldman (@svovaf)
19944 * @since 1.0.5
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 bool|FS_Plugin_Tag
19952 */
19953 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
19954 $this->_logger->entrance();
19955
19956 if ( ! is_numeric( $plugin_id ) ) {
19957 $plugin_id = $this->_plugin->id;
19958 }
19959
19960 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
19961 $updates = $this->get_all_updates();
19962
19963 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
19964 }
19965
19966 /**
19967 * Check if site assigned with active license.
19968 *
19969 * @author Vova Feldman (@svovaf)
19970 * @since 1.0.6
19971 *
19972 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
19973 */
19974 function has_active_license() {
19975 return (
19976 is_object( $this->_license ) &&
19977 is_numeric( $this->_license->id ) &&
19978 ! $this->_license->is_expired()
19979 );
19980 }
19981
19982 /**
19983 * Check if site assigned with active & valid (not expired) license.
19984 *
19985 * @author Vova Feldman (@svovaf)
19986 * @since 1.2.1
19987 *
19988 * @param bool $check_expiration
19989 */
19990 function has_active_valid_license( $check_expiration = true ) {
19991 return self::is_active_valid_license( $this->_license, $check_expiration );
19992 }
19993
19994 /**
19995 * @author Leo Fajardo (@leorw)
19996 * @since 2.3.1
19997 */
19998 function is_data_debug_mode() {
19999 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20000 return false;
20001 }
20002
20003 $fs = $this->is_addon() ?
20004 $this->get_parent_instance() :
20005 $this;
20006
20007 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20008 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20009 } else {
20010 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20011 }
20012
20013 return ( 'true' === $is_developer_license_debug_mode );
20014 }
20015
20016 /**
20017 * @author Leo Fajardo (@leorw)
20018 * @since 2.3.1
20019 */
20020 function _set_data_debug_mode() {
20021 if ( ! $this->is_whitelabeled( true ) ) {
20022 return;
20023 }
20024
20025 $license_or_user_key = fs_request_get( 'license_or_user_key' );
20026
20027 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20028 'true' :
20029 'false';
20030
20031 if ( 'true' === $transient_value ) {
20032 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20033 'last_license_key' :
20034 'last_license_user_key'
20035 );
20036
20037 if ( md5( $license_or_user_key ) !== $stored_key ) {
20038 $this->shoot_ajax_failure( sprintf(
20039 '%s... %s',
20040 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20041 $this->get_text_inline(
20042 'seems like the key you entered doesn\'t match our records.',
20043 'developer-or-license-not-found'
20044 )
20045 ) );
20046 }
20047 }
20048
20049 if ( $this->is_network_active() && fs_is_network_admin() ) {
20050 set_site_transient(
20051 "fs_{$this->get_id()}_data_debug_mode",
20052 $transient_value,
20053 WP_FS__TIME_24_HOURS_IN_SEC / 24
20054 );
20055 } else {
20056 set_transient(
20057 "fs_{$this->get_id()}_data_debug_mode",
20058 $transient_value,
20059 WP_FS__TIME_24_HOURS_IN_SEC / 24
20060 );
20061 }
20062
20063 if ( 'true' === $transient_value ) {
20064 $this->_admin_notices->add_sticky(
20065 $this->get_text_inline(
20066 '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.',
20067 'data_debug_mode_enabled'
20068 ),
20069 'data_debug_mode_enabled'
20070 );
20071 }
20072
20073 $this->shoot_ajax_success();
20074 }
20075
20076 /**
20077 * Check if a given license is active & valid (not expired).
20078 *
20079 * @author Vova Feldman (@svovaf)
20080 * @since 2.1.3
20081 *
20082 * @param FS_Plugin_License $license
20083 * @param bool $check_expiration
20084 *
20085 * @return bool
20086 */
20087 private static function is_active_valid_license( $license, $check_expiration = true ) {
20088 return (
20089 is_object( $license ) &&
20090 FS_Plugin_License::is_valid_id( $license->id ) &&
20091 $license->is_active() &&
20092 ( ! $check_expiration || $license->is_valid() )
20093 );
20094 }
20095
20096 /**
20097 * Checks if there's any site that is associated with an active & valid license.
20098 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20099 *
20100 * @author Vova Feldman (@svovaf)
20101 * @since 2.1.3
20102 *
20103 * @return bool
20104 */
20105 function has_any_active_valid_license() {
20106 if ( ! fs_is_network_admin() ) {
20107 return $this->has_active_valid_license();
20108 }
20109
20110 $installs = $this->get_blog_install_map();
20111 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20112
20113 foreach ( $installs as $blog_id => $install ) {
20114 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20115 continue;
20116 }
20117
20118 foreach ( $all_plugin_licenses as $license ) {
20119 if ( $license->id == $install->license_id ) {
20120 if ( self::is_active_valid_license( $license ) ) {
20121 return true;
20122 }
20123 }
20124 }
20125 }
20126
20127 return false;
20128 }
20129
20130 /**
20131 * Check if site assigned with license with enabled features.
20132 *
20133 * @author Vova Feldman (@svovaf)
20134 * @since 1.0.6
20135 *
20136 * @return bool
20137 */
20138 function has_features_enabled_license() {
20139 return (
20140 is_object( $this->_license ) &&
20141 is_numeric( $this->_license->id ) &&
20142 $this->_license->is_features_enabled()
20143 );
20144 }
20145
20146 /**
20147 * Checks if the product is activated with a bundle license.
20148 *
20149 * @author Leo Fajardo (@leorw)
20150 * @since 2.4.0
20151 *
20152 * @return bool
20153 */
20154 function is_activated_with_bundle_license() {
20155 if ( ! $this->has_features_enabled_license() ) {
20156 return false;
20157 }
20158
20159 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20160 }
20161
20162 /**
20163 * Check if user is a trial or have feature enabled license.
20164 *
20165 * @author Vova Feldman (@svovaf)
20166 * @since 1.1.7
20167 *
20168 * @return bool
20169 */
20170 function can_use_premium_code() {
20171 return $this->is_trial() || $this->has_features_enabled_license();
20172 }
20173
20174 /**
20175 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20176 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20177 * the context user is authenticated.
20178 *
20179 * @author Leo Fajardo (@leorw)
20180 * @since 1.2.2
20181 *
20182 * @return bool
20183 */
20184 function is_user_admin() {
20185 /**
20186 * Require a super-admin when network activated, running from the network level OR if
20187 * running from the site level but not delegated the opt-in.
20188 *
20189 * @author Vova Feldman (@svovaf)
20190 * @since 2.0.0
20191 */
20192 if ( $this->_is_network_active &&
20193 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20194 ) {
20195 return is_super_admin();
20196 }
20197
20198 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20199 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20200 }
20201
20202 /**
20203 * Sync site's plan.
20204 *
20205 * @author Vova Feldman (@svovaf)
20206 * @since 1.0.3
20207 *
20208 * @uses FS_Api
20209 *
20210 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20211 * the admin.
20212 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20213 * network-level "Account" page.
20214 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20215 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20216 * updates for a single site in case `execute_cron` has switched to a different blog.
20217 */
20218 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20219 $this->_logger->entrance();
20220
20221 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20222
20223 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20224
20225 if ( $is_addon_sync ) {
20226 $this->_sync_addon_license( $plugin_id, $background );
20227 } else {
20228 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20229 }
20230
20231 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20232 }
20233
20234 /**
20235 * Sync plugin's add-on license.
20236 *
20237 * @author Vova Feldman (@svovaf)
20238 * @since 1.0.6
20239 * @uses FS_Api
20240 *
20241 * @param number $addon_id
20242 * @param bool $background
20243 */
20244 private function _sync_addon_license( $addon_id, $background ) {
20245 $this->_logger->entrance();
20246
20247 if ( $this->is_addon_activated( $addon_id ) ) {
20248 // If already installed, use add-on sync.
20249 $fs_addon = self::get_instance_by_id( $addon_id );
20250
20251 if (
20252 // Add-on is network activated and network integrated.
20253 $fs_addon->is_network_active() ||
20254 // Background sync cron.
20255 self::is_cron() ||
20256 // Add-on is not network activated or not network integrated.
20257 ! fs_is_network_admin()
20258 ) {
20259 $fs_addon->_sync_license( $background );
20260
20261 return;
20262 }
20263 }
20264
20265 // Validate add-on exists.
20266 $addon = $this->get_addon( $addon_id );
20267
20268 if ( ! is_object( $addon ) ) {
20269 return;
20270 }
20271
20272 // Add add-on into account add-ons.
20273 $account_addons = $this->get_account_addons();
20274 if ( ! is_array( $account_addons ) ) {
20275 $account_addons = array();
20276 }
20277 $account_addons[] = $addon->id;
20278 $account_addons = array_unique( $account_addons );
20279 $this->_store_account_addons( $account_addons );
20280
20281 // Load add-on licenses.
20282 $licenses = $this->_fetch_licenses( $addon->id );
20283
20284 // Sync add-on licenses.
20285 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
20286 $this->_update_licenses( $licenses, $addon->id );
20287
20288 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
20289 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
20290
20291 if ( ! isset( $plans_result->error ) ) {
20292 $plans = array();
20293 foreach ( $plans_result->plans as $plan ) {
20294 $plans[] = new FS_Plugin_Plan( $plan );
20295 }
20296
20297 $this->_admin_notices->add_sticky(
20298 sprintf(
20299 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
20300 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
20301 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
20302 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
20303 $addon->title
20304 ) . ' ' . $this->get_latest_download_link(
20305 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
20306 $addon_id
20307 ),
20308 'addon_plan_upgraded_' . $addon->slug,
20309 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20310 );
20311 }
20312 }
20313 }
20314 }
20315
20316 /**
20317 * Sync site's plugin plan.
20318 *
20319 * @author Vova Feldman (@svovaf)
20320 * @since 1.0.6
20321 * @uses FS_Api
20322 *
20323 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
20324 * @param bool $send_installs_update Since 2.0.0
20325 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
20326 * syncing its license from the network-level "Account" page (e.g.: after
20327 * activating a license only for the single install).
20328 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
20329 * can be used here to switch to the previous blog in case `execute_cron`
20330 * has switched to a different blog.
20331 */
20332 private function _sync_plugin_license(
20333 $background = false,
20334 $send_installs_update = true,
20335 $is_context_single_site = false,
20336 $current_blog_id = null
20337 ) {
20338 $this->_logger->entrance();
20339
20340 $plan_change = 'none';
20341
20342 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
20343
20344 if ( ! $send_installs_update ) {
20345 $site = $this->_site;
20346 } else {
20347 /**
20348 * Sync site info.
20349 *
20350 * @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.
20351 */
20352 if ( $is_site_level_sync ) {
20353 /**
20354 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
20355 *
20356 * @author Leo Fajardo (@leorw)
20357 * @since 2.2.3
20358 */
20359 if ( is_numeric( $current_blog_id ) ) {
20360 $this->switch_to_blog( $current_blog_id );
20361 }
20362
20363 $result = $this->send_install_update( array(), true );
20364 $is_valid = $this->is_api_result_entity( $result );
20365 } else {
20366 $result = $this->send_installs_update( array(), true );
20367 $is_valid = $this->is_api_result_object( $result, 'installs' );
20368 }
20369
20370 if ( ! $is_valid ) {
20371 if ( $is_context_single_site ) {
20372 // Switch back to the main blog so that the following logic will have the right entities.
20373 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20374 }
20375
20376 // Show API messages only if not background sync or if paying customer.
20377 if ( ! $background || $this->is_paying() ) {
20378 // Try to ping API to see if not blocked.
20379 if ( ! FS_Api::test() ) {
20380 /**
20381 * Failed to ping API - blocked!
20382 *
20383 * @author Vova Feldman (@svovaf)
20384 * @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.
20385 */
20386 $api = $this->get_api_site_scope();
20387
20388 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
20389 self::$_global_admin_notices->add(
20390 sprintf(
20391 $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' ),
20392 $this->get_plugin_name(),
20393 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
20394 'api.freemius.com',
20395 'wp.freemius.com'
20396 ) ) ) . '</b>'
20397 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
20398 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
20399 'error',
20400 $background,
20401 'api_blocked'
20402 );
20403 }
20404 } else {
20405 // Authentication params are broken.
20406 $this->_admin_notices->add(
20407 $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 ),
20408 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
20409 'error'
20410 );
20411 }
20412 }
20413
20414 // No reason to continue with license sync while there are API issues.
20415 return;
20416 }
20417
20418 if ( $is_site_level_sync ) {
20419 $site = new FS_Site( $result );
20420 } else {
20421 // Map site addresses to their blog IDs.
20422 $address_to_blog_map = $this->get_address_to_blog_map();
20423
20424 // Find the current context install.
20425 $site = null;
20426 foreach ( $result->installs as $install ) {
20427 if ( $install->id == $this->_site->id ) {
20428 $site = new FS_Site( $install );
20429 } else {
20430 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
20431 $blog_id = $address_to_blog_map[ $address ];
20432
20433 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
20434 }
20435 }
20436 }
20437
20438 // Sync plans.
20439 $this->_sync_plans();
20440 }
20441
20442 // Remove sticky API connectivity message.
20443 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
20444
20445 if ( ! $this->has_paid_plan() ) {
20446 $this->_site = $site;
20447 $this->_store_site(
20448 true,
20449 $is_site_level_sync ?
20450 null :
20451 $this->get_network_install_blog_id()
20452 );
20453 } else {
20454 $context_blog_id = 0;
20455
20456 if ( $is_context_single_site ) {
20457 $context_blog_id = get_current_blog_id();
20458
20459 // Switch back to the main blog in order to properly sync the license.
20460 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20461 }
20462
20463 /**
20464 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
20465 * associated with that ID is not included in the user's licenses collection.
20466 */
20467 $this->_sync_licenses(
20468 $site->license_id,
20469 ( $is_context_single_site ?
20470 $context_blog_id :
20471 null
20472 )
20473 );
20474
20475 if ( $is_context_single_site ) {
20476 $this->switch_to_blog( $context_blog_id );
20477 }
20478
20479 // Check if plan / license changed.
20480 if ( $site->plan_id != $this->_site->plan_id ||
20481 // Check if trial started.
20482 $site->trial_plan_id != $this->_site->trial_plan_id ||
20483 $site->trial_ends != $this->_site->trial_ends ||
20484 // Check if license changed.
20485 $site->license_id != $this->_site->license_id
20486 ) {
20487 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
20488 // New trial started.
20489 $this->_site = $site;
20490 $plan_change = 'trial_started';
20491
20492 // For trial with subscription use-case.
20493 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
20494
20495 if ( is_object( $new_license ) && $new_license->is_valid() ) {
20496 $this->_site = $site;
20497 $this->_update_site_license( $new_license );
20498 $this->_store_licenses();
20499
20500 $this->_sync_site_subscription( $this->_license );
20501 }
20502 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
20503 // Was in trial, but now trial expired and no license ID.
20504 // New trial started.
20505 $this->_site = $site;
20506 $plan_change = 'trial_expired';
20507 } else {
20508 $is_free = $this->is_free_plan();
20509
20510 // Make sure license exist and not expired.
20511 $new_license = is_null( $site->license_id ) ?
20512 null :
20513 $this->_get_license_by_id( $site->license_id );
20514
20515 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
20516 // License cancelled.
20517 $this->_site = $site;
20518 $this->_update_site_license( $new_license );
20519 $this->_store_licenses();
20520
20521 $plan_change = 'cancelled';
20522 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
20523 // The license is expired, so ignore upgrade method.
20524 $this->_site = $site;
20525 } else {
20526 // License changed.
20527 $this->_site = $site;
20528
20529 /**
20530 * IMPORTANT:
20531 * 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.
20532 *
20533 * @author Vova Feldman (@svovaf)
20534 * @since 2.0.0
20535 */
20536 $this->_update_site_license( $new_license );
20537
20538 if ( ! $is_context_single_site &&
20539 fs_is_network_admin() &&
20540 $this->_is_network_active &&
20541 $new_license->quota > 1 &&
20542 get_blog_count() > 1
20543 ) {
20544 // See if license can activated on all sites.
20545 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
20546 if ( ! fs_request_get_bool( 'auto_install' ) ) {
20547 // Open the license activation dialog box on the account page.
20548 add_action( 'admin_footer', array(
20549 &$this,
20550 '_open_license_activation_dialog_box'
20551 ) );
20552 }
20553 }
20554 }
20555
20556 $this->_store_licenses();
20557
20558 $plan_change = $is_free ?
20559 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
20560 ( is_object( $new_license ) ?
20561 'changed' :
20562 'downgraded' );
20563 }
20564 }
20565
20566 // Store updated site info.
20567 $this->_store_site(
20568 true,
20569 $is_site_level_sync ?
20570 null :
20571 $this->get_network_install_blog_id()
20572 );
20573 } else {
20574 if ( ! is_object( $this->_license ) ) {
20575 $this->maybe_update_whitelabel_flag(
20576 FS_Plugin_License::is_valid_id( $site->license_id ) ?
20577 $this->get_license_by_id( $site->license_id ) :
20578 null
20579 );
20580 } else {
20581 $this->maybe_update_whitelabel_flag( $this->_license );
20582
20583 if ( $this->_license->is_expired() ) {
20584 if ( ! $this->has_features_enabled_license() ) {
20585 $this->_deactivate_license();
20586 $plan_change = 'downgraded';
20587 } else {
20588 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
20589
20590 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
20591 /**
20592 * Show the expired license notice every 14 days.
20593 *
20594 * @author Leo Fajardo (@leorw)
20595 * @since 2.3.1
20596 */
20597 $plan_change = 'expired';
20598 }
20599 }
20600 }
20601 }
20602
20603 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
20604 $this->_sync_site_subscription( $this->_license );
20605 }
20606 }
20607
20608 if ( $this->is_addon() || $this->has_addons() ) {
20609 /**
20610 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
20611 * an updated valid user licenses collection will be fetched from the server which is used to also
20612 * update the account add-ons (add-ons the user has licenses for).
20613 *
20614 * @author Leo Fajardo (@leorw)
20615 * @since 2.2.4
20616 */
20617 $this->purge_valid_user_licenses_cache();
20618 }
20619 }
20620
20621 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
20622
20623 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
20624 switch ( $plan_change ) {
20625 case 'none':
20626 if ( ! $background && is_admin() ) {
20627 $plan = $this->is_trial() ?
20628 $this->get_trial_plan() :
20629 $this->get_plan();
20630
20631 if ( $plan->is_free() ) {
20632 $this->_admin_notices->add(
20633 sprintf(
20634 $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' ),
20635 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
20636 ) . ' ' . sprintf(
20637 '<a href="%s">%s</a>',
20638 $this->contact_url(
20639 'bug',
20640 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' ),
20641 strtoupper( $plan->name )
20642 )
20643 ),
20644 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
20645 ),
20646 $hmm_text
20647 );
20648 }
20649 }
20650 break;
20651 case 'upgraded':
20652 case 'activated':
20653 $this->_admin_notices->add_sticky(
20654 ( 'activated' === $plan_change ) ?
20655 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ) :
20656 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) .
20657 $this->get_complete_upgrade_instructions(),
20658 'plan_upgraded',
20659 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20660 );
20661
20662 $this->_admin_notices->remove_sticky( array(
20663 'trial_started',
20664 'trial_promotion',
20665 'trial_expired',
20666 'activation_complete',
20667 'license_expired',
20668 ) );
20669 break;
20670 case 'changed':
20671 $this->_admin_notices->add_sticky(
20672 sprintf(
20673 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
20674 $this->get_plan_title()
20675 ),
20676 'plan_changed'
20677 );
20678
20679 $this->_admin_notices->remove_sticky( array(
20680 'trial_started',
20681 'trial_promotion',
20682 'trial_expired',
20683 'activation_complete',
20684 ) );
20685 break;
20686 case 'downgraded':
20687 $this->_admin_notices->add_sticky(
20688 ($this->has_free_plan() ?
20689 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 ) :
20690 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
20691 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) ) ),
20692 'license_expired',
20693 $hmm_text
20694 );
20695 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20696 break;
20697 case 'cancelled':
20698 $this->_admin_notices->add(
20699 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
20700 sprintf(
20701 '<a href="%s">%s</a>',
20702 $this->contact_url( 'bug' ),
20703 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
20704 ),
20705 $hmm_text,
20706 'error'
20707 );
20708 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20709 break;
20710 case 'expired':
20711 $this->_admin_notices->add_sticky(
20712 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 ),
20713 'license_expired',
20714 $hmm_text
20715 );
20716
20717 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
20718
20719 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20720 break;
20721 case 'trial_started':
20722 $this->_admin_notices->add_sticky(
20723 sprintf(
20724 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
20725 '<i>' . $this->get_plugin_name() . '</i>'
20726 ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ),
20727 'trial_started',
20728 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20729 );
20730
20731 $this->_admin_notices->remove_sticky( array(
20732 'trial_promotion',
20733 ) );
20734 break;
20735 case 'trial_expired':
20736 $this->_admin_notices->add_sticky(
20737 ($this->has_free_plan() ?
20738 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
20739 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
20740 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))),
20741 'trial_expired',
20742 $hmm_text
20743 );
20744 $this->_admin_notices->remove_sticky( array(
20745 'trial_started',
20746 'trial_promotion',
20747 'plan_upgraded',
20748 ) );
20749 break;
20750 }
20751 }
20752
20753 if ( 'none' !== $plan_change ) {
20754 if (
20755 ! is_object( $this->_license ) ||
20756 ! $this->_license->is_whitelabeled
20757 ) {
20758 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
20759 }
20760
20761 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
20762 }
20763 }
20764
20765 /**
20766 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
20767 *
20768 * @author Vova Feldman (@svovaf)
20769 * @since 2.0.0
20770 */
20771 public function _open_license_activation_dialog_box() {
20772 $vars = array( 'license_id' => $this->_site->license_id );
20773 fs_require_once_template( 'js/open-license-activation.php', $vars );
20774 }
20775
20776 /**
20777 * @author Vova Feldman (@svovaf)
20778 * @since 1.0.5
20779 *
20780 * @param bool $background
20781 * @param FS_Plugin_License|null $premium_license
20782 */
20783 protected function _activate_license( $background = false, $premium_license = null ) {
20784 $this->_logger->entrance();
20785
20786 if ( is_null( $premium_license ) ) {
20787 $license_id = fs_request_get( 'license_id' );
20788
20789 if ( is_object( $this->_site ) &&
20790 FS_Plugin_License::is_valid_id( $license_id ) &&
20791 $license_id == $this->_site->license_id
20792 ) {
20793 // License is already activated.
20794 return;
20795 }
20796
20797 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
20798 $this->_get_license_by_id( $license_id ) :
20799 $this->_get_available_premium_license();
20800 }
20801
20802 if ( ! is_object( $premium_license ) ) {
20803 return;
20804 }
20805
20806 if ( ! is_object( $this->_site ) ) {
20807 // Not yet opted-in.
20808 $user = $this->get_current_or_network_user();
20809 if ( ! is_object( $user ) ) {
20810 $user = self::_get_user_by_id( $premium_license->user_id );
20811 }
20812
20813 if ( is_object( $user ) ) {
20814 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
20815 } else {
20816 $this->opt_in(
20817 false,
20818 false,
20819 false,
20820 $premium_license->secret_key
20821 );
20822
20823 return;
20824 }
20825 }
20826
20827
20828 /**
20829 * If the premium license is already associated with the install, just
20830 * update the license reference (activation is not required).
20831 *
20832 * @since 1.1.9
20833 */
20834 if ( $premium_license->id == $this->_site->license_id ) {
20835 // License is already activated.
20836 $this->_update_site_license( $premium_license );
20837 $this->_store_account();
20838
20839 return;
20840 }
20841
20842 if ( $this->_site->user_id != $premium_license->user_id ) {
20843 $api_request_params = array( 'license_key' => $premium_license->secret_key );
20844 } else {
20845 $api_request_params = array();
20846 }
20847
20848 $api = $this->get_api_site_scope();
20849 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
20850
20851 if ( ! $this->is_api_result_entity( $license ) ) {
20852 if ( ! $background ) {
20853 $this->_admin_notices->add( sprintf(
20854 '%s %s',
20855 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
20856 ( is_object( $license ) && isset( $license->error ) ?
20857 $license->error->message :
20858 sprintf( '%s<br><code>%s</code>',
20859 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
20860 var_export( $license, true )
20861 )
20862 )
20863 ),
20864 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
20865 'error'
20866 );
20867 }
20868
20869 return;
20870 }
20871
20872 $premium_license = new FS_Plugin_License( $license );
20873
20874 // Updated site plan.
20875 $site = $this->get_api_site_scope()->get( '/', true );
20876 if ( $this->is_api_result_entity( $site ) ) {
20877 $this->_site = new FS_Site( $site );
20878 }
20879 $this->_update_site_license( $premium_license );
20880
20881 $this->_store_account();
20882
20883 if ( $this->is_addon() || $this->has_addons() ) {
20884 /**
20885 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
20886 * an updated valid user licenses collection will be fetched from the server which is used to also
20887 * update the account add-ons (add-ons the user has licenses for).
20888 *
20889 * @author Leo Fajardo (@leorw)
20890 * @since 2.2.4
20891 */
20892 $this->purge_valid_user_licenses_cache();
20893 }
20894
20895 if ( ! $background ) {
20896 $this->_admin_notices->add_sticky(
20897 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
20898 $this->get_complete_upgrade_instructions(),
20899 'license_activated',
20900 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20901 );
20902 }
20903
20904 $this->_admin_notices->remove_sticky( array(
20905 'trial_promotion',
20906 'license_expired',
20907 ) );
20908 }
20909
20910 /**
20911 * @author Vova Feldman (@svovaf)
20912 * @since 1.0.5
20913 *
20914 * @param bool $show_notice
20915 */
20916 protected function _deactivate_license( $show_notice = true ) {
20917 $this->_logger->entrance();
20918
20919 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
20920
20921 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
20922 $this->_admin_notices->add(
20923 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() ),
20924 $hmm_text
20925 );
20926
20927 return;
20928 }
20929
20930 $api = $this->get_api_site_scope();
20931 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
20932
20933 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
20934 }
20935
20936 /**
20937 * @author Leo Fajardo (@leorw)
20938 * @since 2.2.1
20939 *
20940 * @param FS_Plugin_License $license
20941 * @param bool|string $hmm_text
20942 * @param bool $show_notice
20943 */
20944 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
20945 if ( isset( $license->error ) ) {
20946 $this->_admin_notices->add(
20947 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
20948 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
20949 $hmm_text,
20950 'error'
20951 );
20952
20953 return;
20954 }
20955
20956 // Update license cache.
20957 if ( is_array( $this->_licenses ) ) {
20958 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
20959 if ( $license->id == $this->_licenses[ $i ]->id ) {
20960 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
20961 }
20962 }
20963 }
20964
20965 // Update site plan to default.
20966 $this->_sync_plans();
20967 $this->_site->plan_id = $this->_plans[0]->id;
20968 // Unlink license from site.
20969 $this->_update_site_license( null );
20970
20971 $this->_store_account();
20972
20973 if ( $show_notice ) {
20974 $this->_admin_notices->add(
20975 sprintf( $this->is_only_premium() ?
20976 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
20977 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
20978 $this->get_plan_title()
20979 ),
20980 $this->get_text_inline( 'O.K', 'ok' )
20981 );
20982 }
20983
20984 $this->_admin_notices->remove_sticky( array(
20985 'plan_upgraded',
20986 'license_activated',
20987 ) );
20988 }
20989
20990 /**
20991 * Site plan downgrade.
20992 *
20993 * @author Vova Feldman (@svovaf)
20994 * @since 1.0.4
20995 *
20996 * @return object
20997 *
20998 * @uses FS_Api
20999 */
21000 private function _downgrade_site() {
21001 $this->_logger->entrance();
21002
21003 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21004
21005 $api = $this->get_api_site_scope();
21006 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21007
21008 $plan_downgraded = false;
21009 $plan = false;
21010 if ( $this->is_api_result_entity( $site ) ) {
21011 $prev_plan_id = $this->_site->plan_id;
21012
21013 // Update new site plan id.
21014 $this->_site->plan_id = $site->plan_id;
21015
21016 $plan = $this->get_plan();
21017 $subscription = $this->_sync_site_subscription( $this->_license );
21018
21019 // Plan downgraded if plan was changed or subscription was cancelled.
21020 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21021 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21022 } else {
21023 // handle different error cases.
21024 $this->handle_license_deactivation_result(
21025 $site,
21026 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21027 );
21028 }
21029
21030 if ( ! $plan_downgraded ) {
21031 return (object) array(
21032 'error' => (object) array(
21033 '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' )
21034 )
21035 );
21036 }
21037
21038 // Remove previous sticky message about upgrade (if exist).
21039 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21040
21041 $this->_admin_notices->add(
21042 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21043 $plan->title,
21044 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21045 )
21046 );
21047
21048 // Store site updates.
21049 $this->_store_site();
21050
21051 if ( $deactivate_license &&
21052 ! FS_Plugin_License::is_valid_id( $site->license_id )
21053 ) {
21054 if ( $this->_site->is_localhost() ) {
21055 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21056 } else {
21057 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21058 }
21059
21060 // Handle successful license deactivation result.
21061 $this->handle_license_deactivation_result( $this->_license );
21062 }
21063
21064 return $site;
21065 }
21066
21067 /**
21068 * @author Vova Feldman (@svovaf)
21069 * @since 1.1.8.1
21070 *
21071 * @param bool|string $plan_name
21072 *
21073 * @return bool If trial was successfully started.
21074 */
21075 function start_trial( $plan_name = false ) {
21076 $this->_logger->entrance();
21077
21078 // Alias.
21079 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21080
21081 if ( $this->is_trial() ) {
21082 // Already in trial mode.
21083 $this->_admin_notices->add(
21084 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21085 $oops_text,
21086 'error'
21087 );
21088
21089 return false;
21090 }
21091
21092 if ( $this->_site->is_trial_utilized() ) {
21093 // Trial was already utilized.
21094 $this->_admin_notices->add(
21095 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21096 $oops_text,
21097 'error'
21098 );
21099
21100 return false;
21101 }
21102
21103 if ( false !== $plan_name ) {
21104 $plan = $this->get_plan_by_name( $plan_name );
21105
21106 if ( false === $plan ) {
21107 // Plan doesn't exist.
21108 $this->_admin_notices->add(
21109 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21110 $oops_text,
21111 'error'
21112 );
21113
21114 return false;
21115 }
21116
21117 if ( ! $plan->has_trial() ) {
21118 // Plan doesn't exist.
21119 $this->_admin_notices->add(
21120 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21121 $oops_text,
21122 'error'
21123 );
21124
21125 return false;
21126 }
21127 } else {
21128 if ( ! $this->has_trial_plan() ) {
21129 // None of the plans have a trial.
21130 $this->_admin_notices->add(
21131 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21132 $oops_text,
21133 'error'
21134 );
21135
21136 return false;
21137 }
21138
21139 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21140
21141 $plan = $plans_with_trial[0];
21142 }
21143
21144 $api = $this->get_api_site_scope();
21145 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
21146
21147 if ( ! $this->is_api_result_entity( $plan ) ) {
21148 // Some API error while trying to start the trial.
21149 $this->_admin_notices->add(
21150 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type )
21151 . ' ' . var_export( $plan, true ),
21152 $oops_text,
21153 'error'
21154 );
21155
21156 return false;
21157 }
21158
21159 // Sync license.
21160 $this->_sync_license();
21161
21162 return $this->is_trial();
21163 }
21164
21165 /**
21166 * Cancel site trial.
21167 *
21168 * @author Vova Feldman (@svovaf)
21169 * @since 1.0.9
21170 *
21171 * @return object
21172 *
21173 * @uses FS_Api
21174 */
21175 private function _cancel_trial() {
21176 $this->_logger->entrance();
21177
21178 if ( ! $this->is_trial() ) {
21179 return (object) array(
21180 'error' => (object) array(
21181 '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' )
21182 )
21183 );
21184 }
21185
21186 $trial_plan = $this->get_trial_plan();
21187
21188 $api = $this->get_api_site_scope();
21189 $site = $api->call( 'trials.json', 'delete' );
21190
21191 $trial_cancelled = false;
21192
21193 if ( $this->is_api_result_entity( $site ) ) {
21194 $prev_trial_ends = $this->_site->trial_ends;
21195
21196 if ( $this->is_paid_trial() ) {
21197 $this->_license->expiration = $site->trial_ends;
21198 $this->_license->is_cancelled = true;
21199 $this->_update_site_license( $this->_license );
21200 $this->_store_licenses();
21201
21202 // Clear subscription reference.
21203 $this->_sync_site_subscription( null );
21204 }
21205
21206 // Update site info.
21207 $this->_site = new FS_Site( $site );
21208
21209 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
21210 } else {
21211 // @todo handle different error cases.
21212 }
21213
21214 if ( ! $trial_cancelled ) {
21215 return (object) array(
21216 'error' => (object) array(
21217 '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' )
21218 )
21219 );
21220 }
21221
21222 // Remove previous sticky messages about upgrade or trial (if exist).
21223 $this->_admin_notices->remove_sticky( array(
21224 'trial_started',
21225 'trial_promotion',
21226 'plan_upgraded',
21227 ) );
21228
21229 // Store site updates.
21230 $this->_store_site();
21231
21232 if ( ! $this->is_addon() ||
21233 ! $this->deactivate_premium_only_addon_without_license( true )
21234 ) {
21235 $this->_admin_notices->add(
21236 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
21237 );
21238 }
21239
21240 return $site;
21241 }
21242
21243 /**
21244 * @author Vova Feldman (@svovaf)
21245 * @since 1.0.6
21246 *
21247 * @param bool|number $plugin_id
21248 *
21249 * @return bool
21250 */
21251 private function _is_addon_id( $plugin_id ) {
21252 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
21253 }
21254
21255 /**
21256 * Check if user eligible to download premium version updates.
21257 *
21258 * @author Vova Feldman (@svovaf)
21259 * @since 1.0.6
21260 *
21261 * @return bool
21262 */
21263 private function _can_download_premium() {
21264 return $this->has_any_active_valid_license() ||
21265 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
21266 }
21267
21268 /**
21269 *
21270 * @author Vova Feldman (@svovaf)
21271 * @since 1.0.6
21272 *
21273 * @param bool|number $addon_id
21274 * @param string $type "json" or "zip"
21275 *
21276 * @return string
21277 */
21278 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
21279
21280 $is_addon = $this->_is_addon_id( $addon_id );
21281
21282 $is_premium = null;
21283 if ( ! $is_addon ) {
21284 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
21285 } else if ( $this->is_addon_activated( $addon_id ) ) {
21286 $fs_addon = self::get_instance_by_id( $addon_id );
21287 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
21288 }
21289
21290 // If add-on, then append add-on ID.
21291 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
21292 '/updates/latest.' . $type;
21293
21294 // If add-on and not yet activated, try to fetch based on server licensing.
21295 if ( is_bool( $is_premium ) ) {
21296 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
21297 }
21298
21299 if ( $this->has_secret_key() ) {
21300 $endpoint = add_query_arg( 'type', 'all', $endpoint );
21301 } else if ( $this->is_registered() && $this->_user->is_beta() ) {
21302 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
21303 }
21304
21305 return $endpoint;
21306 }
21307
21308 /**
21309 * @author Vova Feldman (@svovaf)
21310 * @since 1.0.4
21311 *
21312 * @param bool|number $addon_id
21313 * @param bool $flush Since 1.1.7.3
21314 * @param int $expiration Since 1.2.2.7
21315 * @param bool|string $newer_than Since 2.2.1
21316 * @param bool|string $fetch_readme Since 2.2.1
21317 *
21318 * @return object|false Plugin latest tag info.
21319 */
21320 function _fetch_latest_version(
21321 $addon_id = false,
21322 $flush = true,
21323 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
21324 $newer_than = false,
21325 $fetch_readme = true
21326 ) {
21327 $this->_logger->entrance();
21328
21329 $switch_to_blog_id = null;
21330
21331 /**
21332 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
21333 * @since 1.1.7.4 Also check updates for add-ons.
21334 */
21335 if ( ! $this->is_registered() &&
21336 ! $this->_is_addon_id( $addon_id )
21337 ) {
21338 if ( ! is_multisite() ) {
21339 return false;
21340 }
21341
21342 $installs_map = $this->get_blog_install_map();
21343
21344 foreach ( $installs_map as $blog_id => $install ) {
21345 /**
21346 * @var FS_Site $install
21347 */
21348 if ( $install->is_trial() ) {
21349 $switch_to_blog_id = $blog_id;
21350 break;
21351 }
21352
21353 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
21354 $license = $this->get_license_by_id( $install->license_id );
21355
21356 if ( is_object( $license ) && $license->is_features_enabled() ) {
21357 $switch_to_blog_id = $blog_id;
21358 break;
21359 }
21360 }
21361 }
21362
21363 if ( is_null( $switch_to_blog_id ) ) {
21364 return false;
21365 }
21366 }
21367
21368 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
21369 get_current_blog_id() :
21370 0;
21371
21372 if ( is_numeric( $switch_to_blog_id ) ) {
21373 $this->switch_to_blog( $switch_to_blog_id );
21374 }
21375
21376 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
21377
21378 if ( ! empty( $newer_than ) ) {
21379 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
21380 }
21381
21382 if ( true === $fetch_readme ) {
21383 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
21384 }
21385
21386 $tag = $this->get_api_site_or_plugin_scope()->get(
21387 $latest_version_endpoint,
21388 $flush,
21389 $expiration
21390 );
21391
21392 if ( is_numeric( $switch_to_blog_id ) ) {
21393 $this->switch_to_blog( $current_blog_id );
21394 }
21395
21396 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
21397
21398 $this->_logger->departure( 'Latest version ' . $latest_version );
21399
21400 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
21401 }
21402
21403 #----------------------------------------------------------------------------------
21404 #region Download Plugin
21405 #----------------------------------------------------------------------------------
21406
21407 /**
21408 * Download latest plugin version, based on plan.
21409 *
21410 * Not like _download_latest(), this will redirect the page
21411 * to secure download url to prevent dual download (from FS to WP server,
21412 * and then from WP server to the client / browser).
21413 *
21414 * @author Vova Feldman (@svovaf)
21415 * @since 1.0.9
21416 *
21417 * @param bool|number $plugin_id
21418 *
21419 * @uses FS_Api
21420 * @uses wp_redirect()
21421 */
21422 private function download_latest_directly( $plugin_id = false ) {
21423 $this->_logger->entrance();
21424
21425 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
21426 }
21427
21428 /**
21429 * Get latest plugin FS API download URL.
21430 *
21431 * @author Vova Feldman (@svovaf)
21432 * @since 1.0.9
21433 *
21434 * @param bool|number $plugin_id
21435 *
21436 * @return string
21437 */
21438 private function get_latest_download_api_url( $plugin_id = false ) {
21439 $this->_logger->entrance();
21440
21441 return $this->get_api_site_scope()->get_signed_url(
21442 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
21443 );
21444 }
21445
21446 /**
21447 * Get payment invoice URL.
21448 *
21449 * @author Vova Feldman (@svovaf)
21450 * @since 1.2.0
21451 *
21452 * @param bool|number $payment_id
21453 *
21454 * @return string
21455 */
21456 function _get_invoice_api_url( $payment_id = false ) {
21457 $this->_logger->entrance();
21458
21459 $url = $this->get_api_user_scope()->get_signed_url(
21460 "/payments/{$payment_id}/invoice.pdf"
21461 );
21462
21463 if ( ! fs_starts_with( $url, 'https://' ) ) {
21464 // Always use HTTPS for invoices.
21465 $url = 'https' . substr( $url, 4 );
21466 }
21467
21468 return $url;
21469 }
21470
21471 /**
21472 * Get latest plugin download link.
21473 *
21474 * @author Vova Feldman (@svovaf)
21475 * @since 1.0.9
21476 *
21477 * @param string $label
21478 * @param bool|number $plugin_id
21479 *
21480 * @return string
21481 */
21482 private function get_latest_download_link( $label, $plugin_id = false ) {
21483 return sprintf(
21484 '<a target="_blank" rel="noopener" href="%s">%s</a>',
21485 $this->_get_latest_download_local_url( $plugin_id ),
21486 $label
21487 );
21488 }
21489
21490 /**
21491 * Get latest plugin download local URL.
21492 *
21493 * @author Vova Feldman (@svovaf)
21494 * @since 1.0.9
21495 *
21496 * @param bool|number $plugin_id
21497 *
21498 * @return string
21499 */
21500 function _get_latest_download_local_url( $plugin_id = false ) {
21501 // Add timestamp to protect from caching.
21502 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
21503
21504 if ( ! empty( $plugin_id ) ) {
21505 $params['plugin_id'] = $plugin_id;
21506 } else if ( $this->is_addon() ) {
21507 $params['plugin_id'] = $this->get_id();
21508 }
21509
21510 $fs = $this->is_addon() ?
21511 $this->get_parent_instance() :
21512 $this;
21513
21514 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
21515 }
21516
21517 #endregion Download Plugin ------------------------------------------------------------------
21518
21519 /**
21520 * @author Vova Feldman (@svovaf)
21521 * @since 1.0.4
21522 *
21523 * @uses FS_Api
21524 *
21525 * @param bool $background Hints the method if it's a background updates check. If false, it means that
21526 * was initiated by the admin.
21527 * @param bool|number $plugin_id
21528 * @param bool $flush Since 1.1.7.3
21529 * @param int $expiration Since 1.2.2.7
21530 * @param bool|string $newer_than Since 2.2.1
21531 */
21532 private function check_updates(
21533 $background = false,
21534 $plugin_id = false,
21535 $flush = true,
21536 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
21537 $newer_than = false
21538 ) {
21539 $this->_logger->entrance();
21540
21541 // Check if there's a newer version for download.
21542 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
21543
21544 $update = null;
21545 if ( is_object( $new_version ) ) {
21546 $update = new FS_Plugin_Tag( $new_version );
21547
21548 if ( ! $background ) {
21549 $this->_admin_notices->add(
21550 sprintf(
21551 /* translators: %s: Numeric version number (e.g. '2.1.9' */
21552 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
21553 $update->version,
21554 sprintf(
21555 '<a href="%s" target="_blank" rel="noopener">%s</a>',
21556 $this->get_account_url( 'download_latest' ),
21557 sprintf(
21558 /* translators: %s: plan name (e.g. latest "Professional" version) */
21559 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
21560 $this->get_plan_title()
21561 )
21562 )
21563 ),
21564 $this->get_text_inline( 'New', 'new' ) . '!'
21565 );
21566 }
21567 } else if ( false === $new_version && ! $background ) {
21568 $this->_admin_notices->add(
21569 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
21570 $this->get_text_inline( 'You are all good!', 'you-are-good' )
21571 );
21572 }
21573
21574 $this->_store_update( $update, true, $plugin_id );
21575 }
21576
21577 /**
21578 * @author Vova Feldman (@svovaf)
21579 * @since 1.0.4
21580 *
21581 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
21582 *
21583 * @return FS_Plugin[]
21584 *
21585 * @uses FS_Api
21586 */
21587 private function sync_addons( $flush = false ) {
21588 $this->_logger->entrance();
21589
21590 $api = $this->get_api_site_or_plugin_scope();
21591
21592 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
21593
21594 /**
21595 * @since 1.2.1
21596 *
21597 * If there's a cached version of the add-ons and not asking
21598 * for a flush, just use the currently stored add-ons.
21599 */
21600 if ( ! $flush && $api->is_cached( $path ) ) {
21601 $addons = self::get_all_addons();
21602
21603 return isset( $addons[ $this->_plugin->id ] ) ?
21604 $addons[ $this->_plugin->id ] :
21605 array();
21606 }
21607
21608 $result = $api->get( $path, $flush );
21609
21610 $addons = array();
21611 if ( $this->is_api_result_object( $result, 'plugins' ) &&
21612 is_array( $result->plugins )
21613 ) {
21614 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
21615 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
21616 }
21617
21618 $this->_store_addons( $addons, true );
21619 }
21620
21621 return $addons;
21622 }
21623
21624 /**
21625 * Handle user email update.
21626 *
21627 * @author Vova Feldman (@svovaf)
21628 * @since 1.0.3
21629 * @uses FS_Api
21630 *
21631 * @param string $new_email
21632 *
21633 * @return object
21634 */
21635 private function update_email( $new_email ) {
21636 $this->_logger->entrance();
21637
21638
21639 $api = $this->get_api_user_scope();
21640 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
21641 'email' => $new_email,
21642 'after_email_confirm_url' => $this->_get_admin_page_url(
21643 'account',
21644 array( 'fs_action' => 'sync_user' )
21645 ),
21646 ) );
21647
21648 if ( ! isset( $user->error ) ) {
21649 $this->_user->email = $user->email;
21650 $this->_user->is_verified = $user->is_verified;
21651 $this->_store_user();
21652 } else {
21653 // handle different error cases.
21654
21655 }
21656
21657 return $user;
21658 }
21659
21660 #----------------------------------------------------------------------------------
21661 #region API Error Handling
21662 #----------------------------------------------------------------------------------
21663
21664 /**
21665 * @author Vova Feldman (@svovaf)
21666 * @since 1.1.1
21667 *
21668 * @param mixed $result
21669 *
21670 * @return bool Is API result contains an error.
21671 */
21672 private function is_api_error( $result ) {
21673 return FS_Api::is_api_error( $result );
21674 }
21675
21676 /**
21677 * Checks if given API result is a non-empty and not an error object.
21678 *
21679 * @author Vova Feldman (@svovaf)
21680 * @since 1.2.1.5
21681 *
21682 * @param mixed $result
21683 * @param string|null $required_property Optional property we want to verify that is set.
21684 *
21685 * @return bool
21686 */
21687 function is_api_result_object( $result, $required_property = null ) {
21688 return FS_Api::is_api_result_object( $result, $required_property );
21689 }
21690
21691 /**
21692 * Checks if given API result is a non-empty entity object with non-empty ID.
21693 *
21694 * @author Vova Feldman (@svovaf)
21695 * @since 1.2.1.5
21696 *
21697 * @param mixed $result
21698 *
21699 * @return bool
21700 */
21701 private function is_api_result_entity( $result ) {
21702 return FS_Api::is_api_result_entity( $result );
21703 }
21704
21705 #endregion
21706
21707 /**
21708 * Make sure a given argument is an array of a specific type.
21709 *
21710 * @author Vova Feldman (@svovaf)
21711 * @since 1.2.1.5
21712 *
21713 * @param mixed $array
21714 * @param string $class
21715 *
21716 * @return bool
21717 */
21718 private function is_array_instanceof( $array, $class ) {
21719 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
21720 }
21721
21722 /**
21723 * Start install ownership change.
21724 *
21725 * @author Vova Feldman (@svovaf)
21726 * @since 1.1.1
21727 * @uses FS_Api
21728 *
21729 * @param string $candidate_email
21730 *
21731 * @return bool Is ownership change successfully initiated.
21732 */
21733 private function init_change_owner( $candidate_email ) {
21734 $this->_logger->entrance();
21735
21736 $api = $this->get_api_site_scope();
21737 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
21738 'email' => $candidate_email,
21739 'after_confirm_url' => $this->_get_admin_page_url(
21740 'account',
21741 array( 'fs_action' => 'change_owner' )
21742 ),
21743 ) );
21744
21745 return ! $this->is_api_error( $result );
21746 }
21747
21748 /**
21749 * Handle install ownership change.
21750 *
21751 * @author Vova Feldman (@svovaf)
21752 * @since 1.1.1
21753 * @uses FS_Api
21754 *
21755 * @return bool Was ownership change successfully complete.
21756 */
21757 private function complete_change_owner() {
21758 $this->_logger->entrance();
21759
21760 $site_result = $this->get_api_site_scope( true )->get();
21761 $site = new FS_Site( $site_result );
21762 $this->_site = $site;
21763
21764 $user = new FS_User();
21765 $user->id = fs_request_get( 'user_id' );
21766
21767 // Validate install's user and given user.
21768 if ( $user->id != $this->_site->user_id ) {
21769 return false;
21770 }
21771
21772 $user->public_key = fs_request_get( 'user_public_key' );
21773 $user->secret_key = fs_request_get( 'user_secret_key' );
21774
21775 // Fetch new user information.
21776 $this->_user = $user;
21777 $user_result = $this->get_api_user_scope( true )->get();
21778 $user = new FS_User( $user_result );
21779 $this->_user = $user;
21780
21781 $this->_set_account( $user, $site );
21782
21783 return true;
21784 }
21785
21786 /**
21787 * Completes ownership change by license.
21788 *
21789 * @author Leo Fajardo (@leorw)
21790 * @since 2.3.2
21791 *
21792 * @param number $user_id
21793 * @param array[string]number $install_ids_by_slug_map
21794 *
21795 */
21796 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
21797 $this->_logger->entrance();
21798
21799 $this->sync_user_by_current_install( $user_id );
21800
21801 $result = $this->get_api_user_scope( true )->get(
21802 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
21803 );
21804
21805 if ( $this->is_api_result_object( $result, 'installs' ) ) {
21806 $sites = self::get_all_sites( $this->get_module_type() );
21807 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
21808
21809 foreach ( $result->installs as $install ) {
21810 $site = new FS_Site( $install );
21811
21812 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
21813 }
21814
21815 $this->set_account_option( 'sites', $sites, true );
21816 }
21817 }
21818
21819 /**
21820 * Handle user name update.
21821 *
21822 * @author Vova Feldman (@svovaf)
21823 * @since 1.0.9
21824 * @uses FS_Api
21825 *
21826 * @return object
21827 */
21828 private function update_user_name() {
21829 $this->_logger->entrance();
21830 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
21831
21832 $api = $this->get_api_user_scope();
21833 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
21834 'name' => $name,
21835 ) );
21836
21837 if ( ! isset( $user->error ) ) {
21838 $this->_user->first = $user->first;
21839 $this->_user->last = $user->last;
21840 $this->_store_user();
21841 } else {
21842 // handle different error cases.
21843
21844 }
21845
21846 return $user;
21847 }
21848
21849 /**
21850 * Verify user email.
21851 *
21852 * @author Vova Feldman (@svovaf)
21853 * @since 1.0.3
21854 * @uses FS_Api
21855 */
21856 private function verify_email() {
21857 $this->_handle_account_user_sync();
21858
21859 if ( $this->_user->is_verified() ) {
21860 return;
21861 }
21862
21863 $api = $this->get_api_site_scope();
21864 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
21865 'after_email_confirm_url' => $this->_get_admin_page_url(
21866 'account',
21867 array( 'fs_action' => 'sync_user' )
21868 )
21869 ) );
21870
21871 if ( ! isset( $result->error ) ) {
21872 $this->_admin_notices->add( sprintf(
21873 $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' ),
21874 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
21875 ) );
21876 } else {
21877 // handle different error cases.
21878
21879 }
21880 }
21881
21882 /**
21883 * @author Vova Feldman (@svovaf)
21884 * @since 1.1.2
21885 *
21886 * @param array $params
21887 * @param bool|null $network
21888 *
21889 * @return string
21890 */
21891 function get_activation_url( $params = array(), $network = null ) {
21892 if ( $this->is_addon() && $this->has_free_plan() ) {
21893 /**
21894 * @author Vova Feldman (@svovaf)
21895 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
21896 */
21897 return $this->get_parent_instance()->get_activation_url( $params );
21898 }
21899
21900 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
21901 }
21902
21903 /**
21904 * @author Vova Feldman (@svovaf)
21905 * @since 1.2.1.5
21906 *
21907 * @param array $params
21908 *
21909 * @return string
21910 */
21911 function get_reconnect_url( $params = array() ) {
21912 $params['fs_action'] = 'reset_anonymous_mode';
21913 $params['fs_unique_affix'] = $this->get_unique_affix();
21914
21915 return $this->get_activation_url( $params );
21916 }
21917
21918 /**
21919 * Get the URL of the page that should be loaded after the user connect
21920 * or skip in the opt-in screen.
21921 *
21922 * @author Vova Feldman (@svovaf)
21923 * @since 1.1.3
21924 *
21925 * @param string $filter Filter name.
21926 * @param array $params Since 1.2.2.7
21927 * @param bool|null $network
21928 *
21929 * @return string
21930 */
21931 function get_after_activation_url( $filter, $params = array(), $network = null ) {
21932 if ( $this->show_opt_in_on_themes_page() &&
21933 ( fs_request_has( 'pending_activation' ) ||
21934 // For cases when the first time path is set, even though it's a WP.org theme.
21935 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
21936 ) {
21937 $first_time_path = '';
21938 } else {
21939 $first_time_path = $this->_menu->get_first_time_path(
21940 fs_is_network_admin() && $this->_is_network_active
21941 );
21942 }
21943
21944 if ( $this->_is_network_active &&
21945 fs_is_network_admin() &&
21946 ! $this->_menu->has_network_menu() &&
21947 $this->is_network_registered()
21948 ) {
21949 $target_url = $this->get_account_url();
21950 } else {
21951 // Default plugin's page.
21952 $target_url = $this->_get_admin_page_url( '', array(), $network );
21953 }
21954
21955 return add_query_arg( $params, $this->apply_filters(
21956 $filter,
21957 empty( $first_time_path ) ?
21958 $target_url :
21959 $first_time_path
21960 ) );
21961 }
21962
21963 /**
21964 * Handle account page updates / edits / actions.
21965 *
21966 * @author Vova Feldman (@svovaf)
21967 * @since 1.0.2
21968 *
21969 */
21970 private function _handle_account_edits() {
21971 if ( ! $this->is_user_admin() ) {
21972 return;
21973 }
21974
21975 $action = fs_get_action();
21976
21977 if ( empty( $action ) ) {
21978 return;
21979 }
21980
21981 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21982 $install_id = fs_request_get( 'install_id', '' );
21983
21984 // Alias.
21985 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21986
21987 $is_network_action = $this->is_network_level_action();
21988 $blog_id = $this->is_network_level_site_specific_action();
21989 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
21990
21991 if ( is_numeric( $blog_id ) ) {
21992 $this->switch_to_blog( $blog_id );
21993 } else {
21994 $blog_id = '';
21995 }
21996
21997 switch ( $action ) {
21998 case 'opt_in':
21999 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22000
22001 if ( $is_parent_plugin_action ) {
22002 if ( $is_network_action && ! empty( $blog_id ) ) {
22003 if ( ! $this->is_registered() ) {
22004 $this->install_with_user(
22005 $this->get_network_user(),
22006 false,
22007 false,
22008 false,
22009 false
22010 );
22011
22012 $this->_admin_notices->add(
22013 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22014 $this->get_text_inline( 'Awesome', 'awesome' )
22015 );
22016 }
22017 }
22018 }
22019 break;
22020
22021 case 'toggle_tracking':
22022 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22023
22024 if ( $is_parent_plugin_action ) {
22025 if ( $is_network_action && ! empty( $blog_id ) ) {
22026 if ( $this->is_registered() ) {
22027 if ( $this->is_tracking_prohibited() ) {
22028 if ( $this->allow_site_tracking() ) {
22029 $this->_admin_notices->add(
22030 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 ),
22031 $this->get_text_inline( 'Thank you!', 'thank-you' )
22032 );
22033 }
22034 } else {
22035 if ( $this->stop_site_tracking() ) {
22036 $this->_admin_notices->add(
22037 sprintf(
22038 $this->get_text_inline( 'We will no longer be sending any usage data of %s on %s to %s.', 'opted-out-successfully' ),
22039 $this->get_plugin_title(),
22040 fs_strip_url_protocol( get_site_url( $blog_id ) ),
22041 sprintf(
22042 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22043 'https://freemius.com',
22044 'freemius.com'
22045 )
22046 )
22047 );
22048 }
22049 }
22050 }
22051 }
22052 }
22053
22054 break;
22055
22056 case 'delete_account':
22057 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22058
22059 $is_network_deletion = $is_network_action && empty( $blog_id );
22060
22061 if ( $is_parent_plugin_action ) {
22062 // Delete add-on installs if have any.
22063 $installed_addons = $this->get_installed_addons();
22064 foreach ( $installed_addons as $fs_addon ) {
22065 if ( $is_network_deletion ) {
22066 $fs_addon->delete_network_account_event();
22067 } else {
22068 $fs_addon->delete_account_event();
22069 }
22070 }
22071
22072 if ( $is_network_deletion ) {
22073 $this->delete_network_account_event();
22074 } else {
22075 $this->delete_account_event();
22076 }
22077
22078 // Clear user and site.
22079 $this->_site = null;
22080 $this->_user = null;
22081
22082 $this->maybe_set_slug_and_network_menu_exists_flag();
22083
22084 fs_redirect( $this->get_activation_url() );
22085 } else {
22086 if ( $this->is_addon_activated( $plugin_id ) ) {
22087 $fs_addon = self::get_instance_by_id( $plugin_id );
22088
22089 if ( $is_network_deletion ) {
22090 $fs_addon->delete_network_account_event();
22091 } else {
22092 $fs_addon->delete_account_event();
22093 }
22094
22095 fs_redirect( $this->_get_admin_page_url( 'account' ) );
22096 }
22097 }
22098
22099 return;
22100
22101 case 'downgrade_account':
22102 if ( is_numeric( $blog_id ) ) {
22103 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22104 } else {
22105 check_admin_referer( $action );
22106 }
22107
22108 $switch_to_network_install_blog_after_cancellation = (
22109 is_numeric( $blog_id ) &&
22110 $plugin_id == $this->get_id() &&
22111 ! $this->is_trial()
22112 );
22113
22114 $result = $this->cancel_subscription_or_trial( $plugin_id );
22115 if ( $this->is_api_error( $result ) ) {
22116 $this->_admin_notices->add(
22117 $result->error->message,
22118 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22119 'error'
22120 );
22121 }
22122
22123 if ( $switch_to_network_install_blog_after_cancellation ) {
22124 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22125 }
22126
22127 return;
22128
22129 case 'activate_license':
22130 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22131
22132 $fs = $this;
22133 if ( $plugin_id != $this->get_id() ) {
22134 $fs = $this->is_addon_activated( $plugin_id ) ?
22135 self::get_instance_by_id( $plugin_id ) :
22136 null;
22137 }
22138
22139 if ( is_object( $fs ) ) {
22140 $fs->_activate_license();
22141
22142 /**
22143 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
22144 *
22145 * @author Leo Fajardo (@leorw)
22146 * @since 2.4.0
22147 */
22148 unset( $_REQUEST['plugin_id'] );
22149
22150 if ( $this->is_bundle_license_auto_activation_enabled() ) {
22151 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
22152 }
22153 }
22154
22155 return;
22156
22157 case 'deactivate_license':
22158 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22159
22160 if ( $plugin_id == $this->get_id() ) {
22161 $this->_deactivate_license();
22162
22163 if ( $this->is_only_premium() ) {
22164 // Clear user and site.
22165 $this->_site = null;
22166 $this->_user = null;
22167
22168 if ( ! $is_network_action ) {
22169 fs_redirect( $this->get_activation_url() );
22170 } else if ( is_numeric( $blog_id ) ) {
22171 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22172 }
22173 }
22174 } else {
22175 if ( $this->is_addon_activated( $plugin_id ) ) {
22176 $fs_addon = self::get_instance_by_id( $plugin_id );
22177 $fs_addon->_deactivate_license();
22178 }
22179 }
22180
22181 return;
22182
22183 case 'check_updates':
22184 check_admin_referer( $action );
22185 $this->check_updates();
22186
22187 return;
22188
22189 case 'change_owner':
22190 $state = fs_request_get( 'state', 'init' );
22191 switch ( $state ) {
22192 case 'init':
22193 $candidate_email = fs_request_get( 'candidate_email', '' );
22194
22195 if ( $this->init_change_owner( $candidate_email ) ) {
22196 $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>' ) );
22197 }
22198 break;
22199 case 'owner_confirmed':
22200 $candidate_email = fs_request_get( 'candidate_email', '' );
22201
22202 $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>' ) );
22203 break;
22204 case 'candidate_confirmed':
22205 if ( $this->complete_change_owner() ) {
22206 $this->_admin_notices->add_sticky(
22207 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
22208 'ownership_changed',
22209 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
22210 );
22211 } else {
22212 // @todo Handle failed ownership change message.
22213 }
22214 break;
22215 }
22216
22217 return;
22218
22219 case 'update_email':
22220 check_admin_referer( 'update_email' );
22221
22222 $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' );
22223 $result = $this->update_email( $new_email );
22224
22225 if ( isset( $result->error ) ) {
22226 switch ( $result->error->code ) {
22227 case 'user_exist':
22228 $this->_admin_notices->add(
22229 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
22230 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>' ) .
22231 sprintf(
22232 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
22233 $this->get_account_url( 'change_owner', array(
22234 'state' => 'init',
22235 'candidate_email' => $new_email
22236 ) ),
22237 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
22238 ),
22239 $oops_text,
22240 'error'
22241 );
22242 break;
22243 }
22244 } else {
22245 $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' ) );
22246 }
22247
22248 return;
22249
22250 case 'update_user_name':
22251 check_admin_referer( 'update_user_name' );
22252
22253 $result = $this->update_user_name();
22254
22255 if ( isset( $result->error ) ) {
22256 $this->_admin_notices->add(
22257 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
22258 $oops_text,
22259 'error'
22260 );
22261 } else {
22262 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
22263 }
22264
22265 return;
22266
22267 #region Actions that might be called from external links (e.g. email)
22268
22269 case 'cancel_trial':
22270 $result = $this->cancel_subscription_or_trial( $plugin_id );
22271 if ( $this->is_api_error( $result ) ) {
22272 $this->_admin_notices->add(
22273 $result->error->message,
22274 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22275 'error'
22276 );
22277 }
22278
22279 return;
22280
22281 case 'verify_email':
22282 $this->verify_email();
22283
22284 return;
22285
22286 case 'sync_user':
22287 $this->_handle_account_user_sync();
22288
22289 return;
22290
22291 case $this->get_unique_affix() . '_sync_license':
22292 $this->_sync_license();
22293
22294 return;
22295
22296 case 'download_latest':
22297 $this->download_latest_directly( $plugin_id );
22298
22299 return;
22300
22301 #endregion
22302 }
22303
22304 if ( WP_FS__IS_POST_REQUEST ) {
22305 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
22306 foreach ( $properties as $p ) {
22307 if ( 'update_' . $p === $action ) {
22308 check_admin_referer( $action );
22309
22310 $this->_logger->log( $action );
22311
22312 $site_property = substr( $p, strlen( 'site_' ) );
22313 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
22314 $this->get_site()->{$site_property} = $site_property_value;
22315
22316 // Store account after modification.
22317 $this->_store_site();
22318
22319 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
22320
22321 $this->_admin_notices->add( sprintf(
22322 /* translators: %s: User's account property (e.g. email address, name) */
22323 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
22324 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
22325 ) );
22326
22327 return;
22328 }
22329 }
22330 }
22331 }
22332
22333 /**
22334 * Account page resources load.
22335 *
22336 * @author Vova Feldman (@svovaf)
22337 * @since 1.0.6
22338 */
22339 function _account_page_load() {
22340 $this->_logger->entrance();
22341
22342 $this->_logger->info( var_export( $_REQUEST, true ) );
22343
22344 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
22345
22346 if ( $this->has_addons() ) {
22347 wp_enqueue_script( 'plugin-install' );
22348 add_thickbox();
22349
22350 function fs_addons_body_class( $classes ) {
22351 $classes .= ' plugins-php';
22352
22353 return $classes;
22354 }
22355
22356 add_filter( 'admin_body_class', 'fs_addons_body_class' );
22357 }
22358
22359 if ( $this->has_paid_plan() &&
22360 ! $this->has_any_license() &&
22361 ! $this->is_sync_executed() &&
22362 $this->is_tracking_allowed()
22363 ) {
22364 /**
22365 * If no licenses found and no sync job was executed during the last 24 hours,
22366 * just execute the sync job right away (blocking execution).
22367 *
22368 * @since 1.1.7.3
22369 */
22370 $this->run_manual_sync();
22371 }
22372
22373 $this->_handle_account_edits();
22374
22375 if (
22376 is_object( $this->_license ) &&
22377 $this->_license->user_id == $this->_user->id &&
22378 ! $this->is_whitelabeled( true )
22379 ) {
22380 $this->_admin_notices->add(
22381 sprintf(
22382 $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' ),
22383 sprintf(
22384 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
22385 $this->get_text_inline( 'Click here', 'click-here' )
22386 )
22387 ),
22388 '',
22389 'success',
22390 false,
22391 'license_not_whitelabeled'
22392 );
22393 }
22394
22395 $this->do_action( 'account_page_load_before_departure' );
22396 }
22397
22398 /**
22399 * Renders the "Affiliation" page.
22400 *
22401 * @author Leo Fajardo (@leorw)
22402 * @since 1.2.3
22403 */
22404 function _affiliation_page_render() {
22405 $this->_logger->entrance();
22406
22407 $this->fetch_affiliate_and_terms();
22408
22409 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
22410
22411 $vars = array( 'id' => $this->_module_id );
22412 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
22413 }
22414
22415
22416 /**
22417 * Render account page.
22418 *
22419 * @author Vova Feldman (@svovaf)
22420 * @since 1.0.0
22421 */
22422 function _account_page_render() {
22423 $this->_logger->entrance();
22424
22425 $template = 'account.php';
22426 $vars = array( 'id' => $this->_module_id );
22427
22428 /**
22429 * Added filter to the template to allow developers wrapping the template
22430 * in custom HTML (e.g. within a wizard/tabs).
22431 *
22432 * @author Vova Feldman (@svovaf)
22433 * @since 1.2.1.6
22434 */
22435 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
22436 }
22437
22438 /**
22439 * Render account connect page.
22440 *
22441 * @author Vova Feldman (@svovaf)
22442 * @since 1.0.7
22443 */
22444 function _connect_page_render() {
22445 $this->_logger->entrance();
22446
22447 $vars = array( 'id' => $this->_module_id );
22448
22449 /**
22450 * Added filter to the template to allow developers wrapping the template
22451 * in custom HTML (e.g. within a wizard/tabs).
22452 *
22453 * @author Vova Feldman (@svovaf)
22454 * @since 1.2.1.6
22455 */
22456 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
22457 }
22458
22459 /**
22460 * Load required resources before add-ons page render.
22461 *
22462 * @author Vova Feldman (@svovaf)
22463 * @since 1.0.6
22464 */
22465 function _addons_page_load() {
22466 $this->_logger->entrance();
22467
22468 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
22469
22470 wp_enqueue_script( 'plugin-install' );
22471 add_thickbox();
22472
22473 function fs_addons_body_class( $classes ) {
22474 $classes .= ' plugins-php';
22475
22476 return $classes;
22477 }
22478
22479 add_filter( 'admin_body_class', 'fs_addons_body_class' );
22480
22481 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
22482 $this->_admin_notices->add(
22483 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>' ),
22484 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
22485 'update-nag'
22486 );
22487 }
22488 }
22489
22490 /**
22491 * Render add-ons page.
22492 *
22493 * @author Vova Feldman (@svovaf)
22494 * @since 1.0.6
22495 */
22496 function _addons_page_render() {
22497 $this->_logger->entrance();
22498
22499 $vars = array( 'id' => $this->_module_id );
22500
22501 /**
22502 * Added filter to the template to allow developers wrapping the template
22503 * in custom HTML (e.g. within a wizard/tabs).
22504 *
22505 * @author Vova Feldman (@svovaf)
22506 * @since 1.2.1.6
22507 */
22508 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
22509 }
22510
22511 /* Pricing & Upgrade
22512 ------------------------------------------------------------------------------------------------------------------*/
22513 /**
22514 * Render pricing page.
22515 *
22516 * @author Vova Feldman (@svovaf)
22517 * @since 1.0.0
22518 */
22519 function _pricing_page_render() {
22520 $this->_logger->entrance();
22521
22522 $vars = array( 'id' => $this->_module_id );
22523
22524 if ( 'true' === fs_request_get( 'checkout', false ) ) {
22525 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
22526 } else {
22527 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
22528 }
22529 }
22530
22531 /**
22532 * @author Leo Fajardo (@leorw)
22533 * @since 2.3.1
22534 */
22535 function _maybe_add_pricing_ajax_handler() {
22536 if ( ! $this->should_use_external_pricing() ) {
22537 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
22538 }
22539 }
22540
22541 /**
22542 * @author Leo Fajardo (@leorw)
22543 * @since 2.3.1
22544 */
22545 function _fs_pricing_ajax_action_handler() {
22546 $this->check_ajax_referer( 'pricing_ajax_action' );
22547
22548 $result = null;
22549 $pricing_action = fs_request_get( 'pricing_action' );
22550
22551 switch ( $pricing_action ) {
22552 case 'fetch_pricing_data':
22553 $params = array(
22554 'is_enriched' => true,
22555 'trial' => fs_request_get_bool( 'trial' ),
22556 'sandbox' => fs_request_get( 'sandbox' ),
22557 's_ctx_type' => fs_request_get( 's_ctx_type' ),
22558 's_ctx_id' => fs_request_get( 's_ctx_id' ),
22559 's_ctx_ts' => fs_request_get( 's_ctx_ts' ),
22560 's_ctx_secure' => fs_request_get( 's_ctx_secure' ),
22561 );
22562
22563 $bundle_id = $this->get_bundle_id();
22564 $bundle_public_key = $this->get_bundle_public_key();
22565
22566 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
22567
22568 if ( ! $has_bundle_context ) {
22569 $api = $this->get_api_plugin_scope();
22570 } else {
22571 $api = FS_Api::instance(
22572 $bundle_id,
22573 'plugin',
22574 $bundle_id,
22575 $bundle_public_key,
22576 ! $this->is_live(),
22577 false,
22578 $this->get_sdk_version()
22579 );
22580
22581 $params['plugin_id'] = $this->get_id();
22582 $params['plugin_public_key'] = $this->get_public_key();
22583 }
22584
22585 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
22586 break;
22587 case 'start_trial':
22588 $result = $this->opt_in(
22589 false,
22590 false,
22591 false,
22592 false,
22593 false,
22594 fs_request_get( 'plan_id' )
22595 );
22596 }
22597
22598 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
22599 $this->_logger->api_error( $result );
22600
22601 self::shoot_ajax_failure(
22602 isset( $result->error ) ?
22603 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
22604 var_export( $result, true )
22605 );
22606 }
22607
22608 $this->shoot_ajax_success( $result );
22609 }
22610
22611 #----------------------------------------------------------------------------------
22612 #region Contact Us
22613 #----------------------------------------------------------------------------------
22614
22615 /**
22616 * Render contact-us page.
22617 *
22618 * @author Vova Feldman (@svovaf)
22619 * @since 1.0.3
22620 */
22621 function _contact_page_render() {
22622 $this->_logger->entrance();
22623
22624 $vars = array( 'id' => $this->_module_id );
22625
22626 /**
22627 * Added filter to the template to allow developers wrapping the template
22628 * in custom HTML (e.g. within a wizard/tabs).
22629 *
22630 * @author Vova Feldman (@svovaf)
22631 * @since 2.1.3
22632 */
22633 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
22634 }
22635
22636 #endregion ------------------------------------------------------------------------
22637
22638 /**
22639 * Hide all admin notices to prevent distractions.
22640 *
22641 * @author Vova Feldman (@svovaf)
22642 * @since 1.0.3
22643 *
22644 * @uses remove_all_actions()
22645 */
22646 private static function _hide_admin_notices() {
22647 remove_all_actions( 'admin_notices' );
22648 remove_all_actions( 'network_admin_notices' );
22649 remove_all_actions( 'all_admin_notices' );
22650 remove_all_actions( 'user_admin_notices' );
22651 }
22652
22653 static function _clean_admin_content_section_hook() {
22654 self::_hide_admin_notices();
22655
22656 // Hide footer.
22657 echo '<style>#wpfooter { display: none !important; }</style>';
22658 }
22659
22660 /**
22661 * Attach to admin_head hook to hide all admin notices.
22662 *
22663 * @author Vova Feldman (@svovaf)
22664 * @since 1.0.3
22665 */
22666 static function _clean_admin_content_section() {
22667 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
22668 }
22669
22670 /* CSS & JavaScript
22671 ------------------------------------------------------------------------------------------------------------------*/
22672 /* function _enqueue_script($handle, $src) {
22673 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
22674
22675 $this->_logger->entrance( 'script = ' . $url );
22676
22677 wp_enqueue_script( $handle, $url );
22678 }*/
22679
22680 /* SDK
22681 ------------------------------------------------------------------------------------------------------------------*/
22682 private $_user_api;
22683
22684 /**
22685 *
22686 * @author Vova Feldman (@svovaf)
22687 * @since 1.0.2
22688 *
22689 * @param bool $flush
22690 *
22691 * @return FS_Api
22692 */
22693 private function get_api_user_scope( $flush = false ) {
22694 if ( ! isset( $this->_user_api ) || $flush ) {
22695 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
22696 }
22697
22698 return $this->_user_api;
22699 }
22700
22701 /**
22702 * @author Vova Feldman (@svovaf)
22703 * @since 2.0.0
22704 *
22705 * @param \FS_User $user
22706 *
22707 * @return \FS_Api
22708 */
22709 private function get_api_user_scope_by_user( FS_User $user ) {
22710 return FS_Api::instance(
22711 $this->_module_id,
22712 'user',
22713 $user->id,
22714 $user->public_key,
22715 ! $this->is_live(),
22716 $user->secret_key,
22717 $this->get_sdk_version()
22718 );
22719 }
22720
22721 /**
22722 *
22723 * @author Leo Fajardo (@leorw)
22724 * @since 2.0.0
22725 *
22726 * @param bool $flush
22727 *
22728 * @return FS_Api
22729 */
22730 private function get_current_or_network_user_api_scope( $flush = false ) {
22731 if ( ! $this->_is_network_active ||
22732 ( isset( $this->_user ) && $this->_user instanceof FS_User )
22733 ) {
22734 return $this->get_api_user_scope( $flush );
22735 }
22736
22737 $user = $this->get_current_or_network_user();
22738
22739 $this->_user_api = FS_Api::instance(
22740 $this->_module_id,
22741 'user',
22742 $user->id,
22743 $user->public_key,
22744 ! $this->is_live(),
22745 $user->secret_key,
22746 $this->get_sdk_version()
22747 );
22748
22749 return $this->_user_api;
22750 }
22751
22752 private $_site_api;
22753
22754 /**
22755 *
22756 * @author Vova Feldman (@svovaf)
22757 * @since 1.0.2
22758 *
22759 * @param bool $flush
22760 *
22761 * @return FS_Api
22762 */
22763 private function get_api_site_scope( $flush = false ) {
22764 if ( ! isset( $this->_site_api ) || $flush ) {
22765 $this->_site_api = FS_Api::instance(
22766 $this->_module_id,
22767 'install',
22768 $this->_site->id,
22769 $this->_site->public_key,
22770 ! $this->is_live(),
22771 $this->_site->secret_key,
22772 $this->get_sdk_version()
22773 );
22774 }
22775
22776 return $this->_site_api;
22777 }
22778
22779 private $_plugin_api;
22780
22781 /**
22782 * Get plugin public API scope.
22783 *
22784 * @author Vova Feldman (@svovaf)
22785 * @since 1.0.7
22786 *
22787 * @return FS_Api
22788 */
22789 function get_api_plugin_scope() {
22790 if ( ! isset( $this->_plugin_api ) ) {
22791 $this->_plugin_api = FS_Api::instance(
22792 $this->_module_id,
22793 'plugin',
22794 $this->_plugin->id,
22795 $this->_plugin->public_key,
22796 ! $this->is_live(),
22797 false,
22798 $this->get_sdk_version()
22799 );
22800 }
22801
22802 return $this->_plugin_api;
22803 }
22804
22805 /**
22806 * Get bundle public API scope.
22807 *
22808 * @author Vova Feldman (@svovaf)
22809 * @since 2.3.1
22810 *
22811 * @return FS_Api
22812 */
22813 function get_api_bundle_scope() {
22814 return FS_Api::instance(
22815 $this->get_bundle_id(),
22816 'plugin',
22817 $this->get_bundle_id(),
22818 $this->get_bundle_public_key(),
22819 ! $this->is_live(),
22820 false,
22821 $this->get_sdk_version()
22822 );
22823 }
22824
22825 /**
22826 * Get site API scope object (fallback to public plugin scope when not registered).
22827 *
22828 * @author Vova Feldman (@svovaf)
22829 * @since 1.0.7
22830 *
22831 * @return FS_Api
22832 */
22833 function get_api_site_or_plugin_scope() {
22834 return $this->is_registered() ?
22835 $this->get_api_site_scope() :
22836 $this->get_api_plugin_scope();
22837 }
22838
22839 /**
22840 * @author Leo Fajardo (@leorw)
22841 * @since 2.2.3.1
22842 *
22843 * @param object $result
22844 */
22845 private function maybe_modify_api_curl_error_message( $result ) {
22846 if (
22847 'cUrlMissing' !== $result->error->type &&
22848 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
22849 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
22850 ) {
22851 return;
22852 }
22853
22854 $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' ) .
22855 ' ' .
22856 $this->esc_html_inline(
22857 sprintf(
22858 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
22859 implode(
22860 ', ',
22861 $this->apply_filters( 'api_domains', array(
22862 'api.freemius.com',
22863 'wp.freemius.com'
22864 ) )
22865 )
22866 ),
22867 'connectivity-whitelist'
22868 ) .
22869 ' ' .
22870 sprintf(
22871 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
22872 $this->get_module_type()
22873 );
22874 }
22875
22876 /**
22877 * Show trial promotional notice (if any trial exist).
22878 *
22879 * @author Vova Feldman (@svovaf)
22880 * @since 1.0.9
22881 *
22882 * @param FS_Plugin_Plan[] $plans
22883 */
22884 function _check_for_trial_plans( $plans ) {
22885 /**
22886 * 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.
22887 *
22888 * @author Vova Feldman (@svovaf)
22889 * @since 2.1.2
22890 */
22891 if ( ! is_array( $plans ) && is_object( $plans ) ) {
22892 $plans = array( $plans );
22893 }
22894
22895 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
22896 $plans = array();
22897 }
22898
22899 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
22900 }
22901
22902 /**
22903 * During trial promotion the "upgrade" submenu item turns to
22904 * "start trial" to encourage the trial. Since we want to keep
22905 * the same menu item handler and there's no robust way to
22906 * add new arguments to the menu item link's querystring,
22907 * use JavaScript to find the menu item and update the href of
22908 * the link.
22909 *
22910 * @author Vova Feldman (@svovaf)
22911 * @since 1.2.1.5
22912 */
22913 function _fix_start_trial_menu_item_url() {
22914 $template_args = array( 'id' => $this->_module_id );
22915 fs_require_template( 'add-trial-to-pricing.php', $template_args );
22916 }
22917
22918 /**
22919 * Check if module is currently in a trial promotion mode.
22920 *
22921 * @author Vova Feldman (@svovaf)
22922 * @since 1.2.2.7
22923 *
22924 * @return bool
22925 */
22926 function is_in_trial_promotion() {
22927 return $this->_admin_notices->has_sticky( 'trial_promotion' );
22928 }
22929
22930 /**
22931 * Show trial promotional notice (if any trial exist).
22932 *
22933 * @author Vova Feldman (@svovaf)
22934 * @since 1.0.9
22935 *
22936 * @return bool If trial notice added.
22937 */
22938 function _add_trial_notice() {
22939 if ( ! $this->is_user_admin() ) {
22940 return false;
22941 }
22942
22943 if ( ! $this->is_user_in_admin() ) {
22944 return false;
22945 }
22946
22947 if ( $this->_is_network_active ) {
22948 if ( fs_is_network_admin() ) {
22949 // Network level trial is disabled at the moment.
22950 return false;
22951 }
22952
22953 if ( ! $this->is_delegated_connection() ) {
22954 // Only delegated sites should support trials.
22955 return false;
22956 }
22957 }
22958
22959 // Check if trial message is already shown.
22960 if ( $this->is_in_trial_promotion() ) {
22961 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
22962
22963 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
22964
22965 return false;
22966 }
22967
22968 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
22969 // Don't show trial if running the premium code, unless running in DEV mode.
22970 return false;
22971 }
22972
22973 if ( ! $this->has_trial_plan() ) {
22974 // No plans with trial.
22975 return false;
22976 }
22977
22978 if ( ! $this->apply_filters( 'show_trial', true ) ) {
22979 // Developer explicitly asked not to show the trial promo.
22980 return false;
22981 }
22982
22983 if ( $this->is_registered() ) {
22984 // Check if trial already utilized.
22985 if ( $this->_site->is_trial_utilized() ) {
22986 return false;
22987 }
22988
22989 if ( $this->is_paying_or_trial() ) {
22990 // Don't show trial if paying or already in trial.
22991 return false;
22992 }
22993 }
22994
22995 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
22996 // If not yet opted-in/skipped, or pending activation, don't show trial.
22997 return false;
22998 }
22999
23000 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
23001 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
23002
23003 // Show promotion if never shown before and 24 hours after initial activation with FS.
23004 if ( ! $was_promotion_shown_before &&
23005 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
23006 ) {
23007 return false;
23008 }
23009
23010 // OR if promotion was shown before, try showing it every 30 days.
23011 if ( $was_promotion_shown_before &&
23012 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
23013 ) {
23014 return false;
23015 }
23016
23017 $trial_period = $this->_trial_days;
23018 $require_payment = $this->_is_trial_require_payment;
23019 $trial_url = $this->get_trial_url();
23020 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
23021
23022 if ( $this->is_registered() ) {
23023 // If opted-in, override trial with up to date data from API.
23024 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
23025 $trial_plans_count = count( $trial_plans );
23026
23027 if ( 0 === $trial_plans_count ) {
23028 // If there's no plans with a trial just exit.
23029 return false;
23030 }
23031
23032 /**
23033 * @var FS_Plugin_Plan $paid_plan
23034 */
23035 $paid_plan = $trial_plans[0];
23036 $require_payment = $paid_plan->is_require_subscription;
23037 $trial_period = $paid_plan->trial_period;
23038
23039 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
23040
23041 if ( $total_paid_plans !== $trial_plans_count ) {
23042 // Not all paid plans have a trial - generate a string of those that have it.
23043 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
23044 $plans_string .= sprintf(
23045 ' <a href="%s">%s</a>',
23046 $trial_url,
23047 $trial_plans[ $i ]->title
23048 );
23049
23050 if ( $i < $trial_plans_count - 2 ) {
23051 $plans_string .= ', ';
23052 } else if ( $i == $trial_plans_count - 2 ) {
23053 $plans_string .= ' and ';
23054 }
23055 }
23056 }
23057 }
23058
23059 $message = sprintf(
23060 $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' ),
23061 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
23062 $plans_string,
23063 $trial_period
23064 );
23065
23066 // "No Credit-Card Required" or "No Commitment for N Days".
23067 $cc_string = $require_payment ?
23068 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
23069 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
23070
23071
23072 // Start trial button.
23073 $button = ' ' . sprintf(
23074 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
23075 $trial_url,
23076 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
23077 );
23078
23079 $this->_admin_notices->add_sticky(
23080 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
23081 'trial_promotion',
23082 '',
23083 'promotion'
23084 );
23085
23086 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
23087
23088 return true;
23089 }
23090
23091 /**
23092 * Lets users/customers know that the product has an affiliate program.
23093 *
23094 * @author Leo Fajardo (@leorw)
23095 * @since 1.2.2.11
23096 *
23097 * @return bool Returns true if the notice has been added.
23098 */
23099 function _add_affiliate_program_notice() {
23100 if ( ! $this->is_user_admin() ) {
23101 return false;
23102 }
23103
23104 if ( ! $this->is_user_in_admin() ) {
23105 return false;
23106 }
23107
23108 // Check if the notice is already shown.
23109 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
23110 return false;
23111 }
23112
23113 if (
23114 // Product has no affiliate program.
23115 ! $this->has_affiliate_program() ||
23116 // User has applied for an affiliate account.
23117 ! empty( $this->_storage->affiliate_application_data )
23118 ) {
23119 return false;
23120 }
23121
23122 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
23123 // Developer explicitly asked not to show the notice about the affiliate program.
23124 return false;
23125 }
23126
23127 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23128 // If not yet opted in/skipped, or pending activation, don't show the notice.
23129 return false;
23130 }
23131
23132 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
23133 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
23134
23135 /**
23136 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
23137 * activation with FS.
23138 */
23139 if ( $was_notice_shown_before ||
23140 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
23141 ) {
23142 return false;
23143 }
23144
23145 if ( ! $this->is_paying() &&
23146 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
23147 ) {
23148 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
23149 return false;
23150 }
23151
23152 $message = sprintf(
23153 $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' ),
23154 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
23155 $this->get_module_label( true )
23156 );
23157
23158 // HTML code for the "Learn more..." button.
23159 $button = ' ' . sprintf(
23160 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
23161 $this->_get_admin_page_url( 'affiliation' ),
23162 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
23163 );
23164
23165 $this->_admin_notices->add_sticky(
23166 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
23167 'affiliate_program',
23168 '',
23169 'promotion'
23170 );
23171
23172 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
23173
23174 return true;
23175 }
23176
23177 /**
23178 * @author Vova Feldman (@svovaf)
23179 * @since 1.2.1.5
23180 */
23181 function _enqueue_common_css() {
23182 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
23183 // Add basic CSS for admin-notices and menu-item colors.
23184 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23185 }
23186 }
23187
23188 /**
23189 * @author Leo Fajardo (@leorw)
23190 * @since 1.2.2
23191 */
23192 function _show_theme_activation_optin_dialog() {
23193 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
23194
23195 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
23196 }
23197
23198 /**
23199 * @author Leo Fajardo (@leorw)
23200 * @since 1.2.2
23201 */
23202 function _add_fs_theme_activation_dialog() {
23203 global $pagenow;
23204
23205 if ( 'themes.php' !== $pagenow ) {
23206 return;
23207 }
23208
23209 $vars = array( 'id' => $this->_module_id );
23210 fs_require_once_template( 'connect.php', $vars );
23211 }
23212
23213 /* Action Links
23214 ------------------------------------------------------------------------------------------------------------------*/
23215 private $_action_links_hooked = false;
23216 private $_action_links = array();
23217
23218 /**
23219 * Hook to plugin action links filter.
23220 *
23221 * @author Vova Feldman (@svovaf)
23222 * @since 1.0.0
23223 */
23224 private function hook_plugin_action_links() {
23225 $this->_logger->entrance();
23226
23227 $this->_action_links_hooked = true;
23228
23229 $this->_logger->log( 'Adding action links hooks.' );
23230
23231 // Add action link to settings page.
23232 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
23233 &$this,
23234 '_modify_plugin_action_links_hook'
23235 ), WP_FS__DEFAULT_PRIORITY, 2 );
23236 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
23237 &$this,
23238 '_modify_plugin_action_links_hook'
23239 ), WP_FS__DEFAULT_PRIORITY, 2 );
23240 }
23241
23242 /**
23243 * Add plugin action link.
23244 *
23245 * @author Vova Feldman (@svovaf)
23246 * @since 1.0.0
23247 *
23248 * @param $label
23249 * @param $url
23250 * @param bool $external
23251 * @param int $priority
23252 * @param bool $key
23253 */
23254 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
23255 $this->_logger->entrance();
23256
23257 if ( ! isset( $this->_action_links[ $priority ] ) ) {
23258 $this->_action_links[ $priority ] = array();
23259 }
23260
23261 if ( false === $key ) {
23262 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
23263 }
23264
23265 $this->_action_links[ $priority ][] = array(
23266 'label' => $label,
23267 'href' => $url,
23268 'key' => $key,
23269 'external' => $external
23270 );
23271 }
23272
23273 /**
23274 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
23275 *
23276 * @author Vova Feldman (@svovaf)
23277 * @since 1.0.0
23278 */
23279 function _add_upgrade_action_link() {
23280 $this->_logger->entrance();
23281
23282 $is_activation_mode = $this->is_activation_mode();
23283
23284 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
23285
23286 /**
23287 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
23288 * and "Add-Ons" menus should be added.
23289 *
23290 * @author Leo Fajardo (@leorw)
23291 * @since 2.3.0
23292 */
23293 $add_upgrade_link = (
23294 $add_action_links ||
23295 ( $is_activation_mode && $this->is_only_premium() )
23296 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
23297
23298 $add_addons_link = ( $add_action_links && $this->has_addons() );
23299
23300 if ( ! $add_upgrade_link && ! $add_addons_link ) {
23301 return;
23302 }
23303
23304 if (
23305 $add_upgrade_link &&
23306 $this->is_pricing_page_visible() &&
23307 $this->is_submenu_item_visible( 'pricing' )
23308 ) {
23309 $this->add_plugin_action_link(
23310 $this->get_text_inline( 'Upgrade', 'upgrade' ),
23311 $this->get_upgrade_url(),
23312 false,
23313 7,
23314 'upgrade'
23315 );
23316 }
23317
23318 if (
23319 $add_addons_link &&
23320 $this->has_addons() &&
23321 $this->is_submenu_item_visible( 'addons' )
23322 ) {
23323 $this->add_plugin_action_link(
23324 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
23325 $this->_get_admin_page_url( 'addons' ),
23326 false,
23327 9,
23328 'addons'
23329 );
23330 }
23331 }
23332
23333 /**
23334 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
23335 *
23336 * @author Leo Fajardo (@leorw)
23337 * @since 1.1.9
23338 */
23339 function _add_license_action_link() {
23340 $this->_logger->entrance();
23341
23342 if ( ! self::is_ajax() ) {
23343 // Inject license activation dialog UI and client side code.
23344 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
23345 }
23346
23347 $link_text = $this->is_free_plan() ?
23348 $this->get_text_inline( 'Activate License', 'activate-license' ) :
23349 $this->get_text_inline( 'Change License', 'change-license' );
23350
23351 $this->add_plugin_action_link(
23352 $link_text,
23353 '#',
23354 false,
23355 11,
23356 ( 'activate-license ' . $this->get_unique_affix() )
23357 );
23358 }
23359
23360 /**
23361 * @author Leo Fajardo (@leorw)
23362 * @since 2.0.2
23363 */
23364 function _add_premium_version_upgrade_selection_action() {
23365 $this->_logger->entrance();
23366
23367 if ( ! self::is_ajax() ) {
23368 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
23369 }
23370 }
23371
23372 /**
23373 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
23374 *
23375 * @author Leo Fajardo (@leorw)
23376 * @since 1.2.1.5
23377 */
23378 function _add_tracking_links() {
23379 if ( ! current_user_can( 'manage_options' ) ) {
23380 return;
23381 }
23382
23383 $this->_logger->entrance();
23384
23385 /**
23386 * @author Vova Feldman (@svovaf)
23387 * @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.
23388 */
23389 /*if ( $this->is_premium() ) {
23390 // Don't add opt-in/out for premium code base.
23391 return;
23392 }*/
23393
23394 if ( $this->is_only_premium() && $this->is_free_plan() ) {
23395 // 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.
23396 return;
23397 }
23398
23399 if ( fs_is_network_admin() ) {
23400 if ( ! $this->_is_network_active ) {
23401 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
23402 return;
23403 } else if ( $this->is_network_delegated_connection() ) {
23404 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
23405 return;
23406 }
23407 } else {
23408 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
23409 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
23410 return;
23411 }
23412 }
23413
23414 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
23415 if ( ! $this->is_registered() && $this->is_anonymous() ) {
23416 $this->connect_again();
23417
23418 return;
23419 }
23420 }
23421
23422 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
23423 ( $this->is_theme() && ! self::is_themes_page() )
23424 ) {
23425 // Only show tracking links on the plugins and themes pages.
23426 return;
23427 }
23428
23429 if (
23430 $this->is_activation_mode() &&
23431 $this->is_premium() &&
23432 ! $this->is_registered()
23433 ) {
23434 // If not yet registered and running the premium code base, a license activation link will already be shown.
23435 return;
23436 }
23437
23438 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23439 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
23440 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
23441 return;
23442 }
23443 }
23444
23445 if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) {
23446 return;
23447 }
23448
23449 if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) {
23450 return;
23451 }
23452
23453 if ( $this->add_ajax_action( 'update_tracking_permission', array( &$this, '_update_tracking_permission_callback' ) ) ) {
23454 return;
23455 }
23456
23457 $link_text_id = '';
23458 $url = '#';
23459
23460 if ( $this->is_registered() ) {
23461 if ( $this->is_tracking_allowed() ) {
23462 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
23463 } else {
23464 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
23465 }
23466 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
23467 /**
23468 * Show opt-in link only if skipped or in activation mode.
23469 */
23470 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
23471
23472 $params = ! $this->is_anonymous() ?
23473 array() :
23474 array(
23475 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
23476 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
23477 );
23478
23479 $url = $this->get_activation_url( $params );
23480 }
23481
23482 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
23483
23484 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
23485 $this->add_plugin_action_link(
23486 $link_text_id,
23487 $url,
23488 false,
23489 13,
23490 "opt-in-or-opt-out {$this->_slug}"
23491 );
23492 }
23493 }
23494
23495 /**
23496 * Get the URL of the page that should be loaded right after the plugin activation.
23497 *
23498 * @author Vova Feldman (@svovaf)
23499 * @since 1.1.7.4
23500 *
23501 * @return string
23502 */
23503 function get_after_plugin_activation_redirect_url() {
23504 $url = false;
23505
23506 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
23507 $first_time_path = $this->_menu->get_first_time_path(
23508 fs_is_network_admin() && $this->_is_network_active
23509 );
23510
23511 if ( $this->is_activation_mode() ) {
23512 $url = $this->get_activation_url();
23513 } else if ( ! empty( $first_time_path ) ) {
23514 $url = $first_time_path;
23515 } else {
23516 $page = '';
23517 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
23518 if ( $this->is_network_registered() ) {
23519 $page = 'account';
23520 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
23521 $this->maybe_set_slug_and_network_menu_exists_flag();
23522 }
23523 }
23524
23525 $url = $this->_get_admin_page_url( $page );
23526 }
23527 } else {
23528 $plugin_fs = false;
23529
23530 if ( $this->is_parent_plugin_installed() ) {
23531 $plugin_fs = self::get_parent_instance();
23532 }
23533
23534 if ( is_object( $plugin_fs ) ) {
23535 if ( ! $plugin_fs->is_registered() ) {
23536 // Forward to parent plugin connect when parent not registered.
23537 $url = $plugin_fs->get_activation_url();
23538 } else {
23539 // Forward to account page.
23540 $url = $plugin_fs->_get_admin_page_url( 'account' );
23541 }
23542 }
23543 }
23544
23545 return $url;
23546 }
23547
23548 /**
23549 * Forward page to activation page.
23550 *
23551 * @author Vova Feldman (@svovaf)
23552 * @since 1.0.3
23553 */
23554 function _redirect_on_activation_hook() {
23555 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
23556 $url = $this->get_after_plugin_activation_redirect_url();
23557
23558 if ( is_string( $url ) ) {
23559 fs_redirect( $url );
23560 }
23561 }
23562 }
23563
23564 /**
23565 * Modify plugin's page action links collection.
23566 *
23567 * @author Vova Feldman (@svovaf)
23568 * @since 1.0.0
23569 *
23570 * @param array $links
23571 * @param $file
23572 *
23573 * @return array
23574 */
23575 function _modify_plugin_action_links_hook( $links, $file ) {
23576 $this->_logger->entrance();
23577
23578 $passed_deactivate = false;
23579 $deactivate_link = '';
23580 $before_deactivate = array();
23581 $after_deactivate = array();
23582 foreach ( $links as $key => $link ) {
23583 if ( 'deactivate' === $key ) {
23584 $deactivate_link = $link;
23585 $passed_deactivate = true;
23586 continue;
23587 }
23588
23589 if ( ! $passed_deactivate ) {
23590 $before_deactivate[ $key ] = $link;
23591 } else {
23592 $after_deactivate[ $key ] = $link;
23593 }
23594 }
23595
23596 ksort( $this->_action_links );
23597
23598 foreach ( $this->_action_links as $new_links ) {
23599 foreach ( $new_links as $link ) {
23600 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
23601 }
23602 }
23603
23604 if ( ! empty( $deactivate_link ) ) {
23605 /**
23606 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
23607 *
23608 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
23609 */
23610 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
23611
23612 // Append deactivation link.
23613 $before_deactivate['deactivate'] = $deactivate_link;
23614 }
23615
23616 return array_merge( $before_deactivate, $after_deactivate );
23617 }
23618
23619 /**
23620 * Adds admin message.
23621 *
23622 * @author Vova Feldman (@svovaf)
23623 * @since 1.0.4
23624 *
23625 * @param string $message
23626 * @param string $title
23627 * @param string $type
23628 */
23629 function add_admin_message( $message, $title = '', $type = 'success' ) {
23630 $this->_admin_notices->add( $message, $title, $type );
23631 }
23632
23633 /**
23634 * Adds sticky admin message.
23635 *
23636 * @author Vova Feldman (@svovaf)
23637 * @since 1.1.0
23638 *
23639 * @param string $message
23640 * @param string $id
23641 * @param string $title
23642 * @param string $type
23643 */
23644 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
23645 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
23646 }
23647
23648 /**
23649 * Check if the paid version of the module is installed.
23650 *
23651 * @author Vova Feldman (@svovaf)
23652 * @since 2.2.0
23653 *
23654 * @return bool
23655 */
23656 private function is_premium_version_installed() {
23657 $premium_plugin_basename = $this->premium_plugin_basename();
23658 $premium_plugin = get_plugins( '/' . dirname( $premium_plugin_basename ) );
23659
23660 return ! empty( $premium_plugin );
23661 }
23662
23663 /**
23664 * Helper function that returns the final steps for the upgrade completion.
23665 *
23666 * If the module is already running the premium code, returns an empty string.
23667 *
23668 * @author Vova Feldman (@svovaf)
23669 * @since 1.2.1
23670 *
23671 * @param string $plan_title
23672 *
23673 * @return string
23674 */
23675 private function get_complete_upgrade_instructions( $plan_title = '' ) {
23676 $this->_logger->entrance();
23677
23678 $activate_license_string = $this->get_license_network_activation_notice();
23679
23680 if ( ! $this->has_premium_version() || $this->is_premium() ) {
23681 return '' . $activate_license_string;
23682 }
23683
23684 if ( empty( $plan_title ) ) {
23685 $plan_title = $this->get_plan_title();
23686 }
23687
23688 if ( $this->is_premium_version_installed() ) {
23689 /**
23690 * If the premium version is already installed, instead of showing the installation instructions,
23691 * tell the current user to activate it.
23692 *
23693 * @author Leo Fajardo (@leorw)
23694 * @since 2.2.1
23695 */
23696 $premium_plugin_basename = $this->premium_plugin_basename();
23697
23698 return sprintf(
23699 /* translators: %1$s: Product title; %2$s: Plan title */
23700 $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' ),
23701 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
23702 $plan_title,
23703 sprintf(
23704 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
23705 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_plugin_basename, 'activate-plugin_' . $premium_plugin_basename ),
23706 esc_html( sprintf(
23707 /* translators: %s: Plan title */
23708 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
23709 $plan_title
23710 ) )
23711 )
23712 );
23713 } else {
23714 // @since 1.2.1.5 The free version is auto deactivated.
23715 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
23716 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
23717 '';
23718
23719 return sprintf(
23720 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
23721 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
23722 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
23723 $this->get_latest_download_link( sprintf(
23724 /* translators: %s: Plan title */
23725 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
23726 $plan_title
23727 ) ),
23728 $deactivation_step,
23729 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
23730 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/upload-wp-' . $this->_module_type . 's' ),
23731 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
23732 );
23733 }
23734 }
23735
23736 /**
23737 * @author Leo Fajardo (@leorw)
23738 * @since 2.1.0
23739 *
23740 * @param string $url
23741 * @param array $request
23742 */
23743 private static function enrich_request_for_debug( &$url, &$request ) {
23744 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
23745 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
23746 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
23747
23748 $request['cookies'] = array(
23749 new WP_Http_Cookie( array(
23750 'name' => 'XDEBUG_SESSION',
23751 'value' => 'PHPSTORM',
23752 ) )
23753 );
23754 }
23755 }
23756
23757 /**
23758 * @author Leo Fajardo (@leorw)
23759 * @since 2.1.0
23760 *
23761 * @param string $url
23762 * @param array $request
23763 * @param int $success_cache_expiration
23764 * @param int $failure_cache_expiration
23765 * @param bool $maybe_enrich_request_for_debug
23766 *
23767 * @return WP_Error|array
23768 */
23769 static function safe_remote_post(
23770 &$url,
23771 $request,
23772 $success_cache_expiration = 0,
23773 $failure_cache_expiration = 0,
23774 $maybe_enrich_request_for_debug = true
23775 ) {
23776 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
23777
23778 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
23779
23780 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
23781 get_transient( $cache_key ) :
23782 false;
23783
23784 if ( false === $response ) {
23785 if ( $maybe_enrich_request_for_debug ) {
23786 self::enrich_request_for_debug( $url, $request );
23787 }
23788
23789 $response = wp_remote_post( $url, $request );
23790
23791 if ( $response instanceof WP_Error ) {
23792 if ( 'https://' === substr( $url, 0, 8 ) &&
23793 isset( $response->errors ) &&
23794 isset( $response->errors['http_request_failed'] )
23795 ) {
23796 $http_error = strtolower( $response->errors['http_request_failed'][0] );
23797
23798 if ( false !== strpos( $http_error, 'ssl' ) ||
23799 false !== strpos( $http_error, 'curl error 35' )
23800 ) {
23801 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
23802 $url = 'http://' . substr( $url, 8 );
23803
23804 $request['timeout'] = 15;
23805 $response = wp_remote_post( $url, $request );
23806 }
23807 }
23808 }
23809
23810 if ( false !== $cache_key ) {
23811 set_transient(
23812 $cache_key,
23813 $response,
23814 ( ( $response instanceof WP_Error ) ?
23815 $failure_cache_expiration :
23816 $success_cache_expiration )
23817 );
23818 }
23819 }
23820
23821 return $response;
23822 }
23823
23824 /**
23825 * This method is used to enrich the after upgrade notice instructions when the upgraded
23826 * license cannot be activated network wide (license quota isn't large enough).
23827 *
23828 * @author Vova Feldman (@svovaf)
23829 * @since 2.0.0
23830 *
23831 * @return string
23832 */
23833 private function get_license_network_activation_notice() {
23834 if ( ! $this->_is_network_active ) {
23835 // Module isn't network level activated.
23836 return '';
23837 }
23838
23839 if ( ! fs_is_network_admin() ) {
23840 // Not network level admin.
23841 return '';
23842 }
23843
23844 if ( get_blog_count() == 1 ) {
23845 // There's only a single site in the network so if there's a context license it was already activated.
23846 return '';
23847 }
23848
23849 if ( ! is_object( $this->_license ) ) {
23850 // No context license.
23851 return '';
23852 }
23853
23854 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
23855 // 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).
23856 return '';
23857 }
23858
23859 if ( $this->can_activate_license_on_network( $this->_license ) ) {
23860 // 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).
23861 return '';
23862 }
23863
23864 return sprintf(
23865 $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' ),
23866 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
23867 '</a>'
23868 );
23869 }
23870
23871 /**
23872 * @author Vova Feldman (@svovaf)
23873 * @since 1.2.1.7
23874 *
23875 * @param string $key
23876 *
23877 * @return string
23878 */
23879 function get_text( $key ) {
23880 return fs_text( $key, $this->_slug );
23881 }
23882
23883 /**
23884 * @author Vova Feldman (@svovaf)
23885 * @since 1.2.3
23886 *
23887 * @param string $text Translatable string.
23888 * @param string $key String key for overrides.
23889 *
23890 * @return string
23891 */
23892 function get_text_inline( $text, $key = '' ) {
23893 return _fs_text_inline( $text, $key, $this->_slug );
23894 }
23895
23896 /**
23897 * @author Vova Feldman (@svovaf)
23898 * @since 1.2.3
23899 *
23900 * @param string $text Translatable string.
23901 * @param string $context Context information for the translators.
23902 * @param string $key String key for overrides.
23903 *
23904 * @return string
23905 */
23906 function get_text_x_inline( $text, $context, $key ) {
23907 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
23908 }
23909
23910 /**
23911 * @author Vova Feldman (@svovaf)
23912 * @since 1.2.3
23913 *
23914 * @param string $text Translatable string.
23915 * @param string $key String key for overrides.
23916 *
23917 * @return string
23918 */
23919 function esc_html_inline( $text, $key ) {
23920 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
23921 }
23922
23923 #----------------------------------------------------------------------------------
23924 #region Versioning
23925 #----------------------------------------------------------------------------------
23926
23927 /**
23928 * Check if Freemius in SDK upgrade mode.
23929 *
23930 * @author Vova Feldman (@svovaf)
23931 * @since 1.0.9
23932 *
23933 * @return bool
23934 */
23935 function is_sdk_upgrade_mode() {
23936 return isset( $this->_storage->sdk_upgrade_mode ) ?
23937 $this->_storage->sdk_upgrade_mode :
23938 false;
23939 }
23940
23941 /**
23942 * Turn SDK upgrade mode off.
23943 *
23944 * @author Vova Feldman (@svovaf)
23945 * @since 1.0.9
23946 */
23947 function set_sdk_upgrade_complete() {
23948 $this->_storage->sdk_upgrade_mode = false;
23949 }
23950
23951 /**
23952 * Check if plugin upgrade mode.
23953 *
23954 * @author Vova Feldman (@svovaf)
23955 * @since 1.0.9
23956 *
23957 * @return bool
23958 */
23959 function is_plugin_upgrade_mode() {
23960 return isset( $this->_storage->plugin_upgrade_mode ) ?
23961 $this->_storage->plugin_upgrade_mode :
23962 false;
23963 }
23964
23965 /**
23966 * Turn plugin upgrade mode off.
23967 *
23968 * @author Vova Feldman (@svovaf)
23969 * @since 1.0.9
23970 */
23971 function set_plugin_upgrade_complete() {
23972 $this->_storage->plugin_upgrade_mode = false;
23973
23974 $license_migration = ! empty( $this->_storage->license_migration ) ?
23975 $this->_storage->license_migration :
23976 array();
23977
23978 $license_migration['is_migrating'] = false;
23979
23980 $this->_storage->license_migration = $license_migration;
23981 }
23982
23983 #endregion
23984
23985 #----------------------------------------------------------------------------------
23986 #region Permissions
23987 #----------------------------------------------------------------------------------
23988
23989 /**
23990 * Check if specific permission requested.
23991 *
23992 * @author Vova Feldman (@svovaf)
23993 * @since 1.1.6
23994 *
23995 * @param string $permission
23996 *
23997 * @return bool
23998 */
23999 function is_permission_requested( $permission ) {
24000 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
24001 }
24002
24003 #endregion
24004
24005 #----------------------------------------------------------------------------------
24006 #region Auto Activation
24007 #----------------------------------------------------------------------------------
24008
24009 /**
24010 * Hints the SDK if running an auto-installation.
24011 *
24012 * @var bool
24013 */
24014 private $_isAutoInstall = false;
24015
24016 /**
24017 * After upgrade callback to install and auto activate a plugin.
24018 * This code will only be executed on explicit request from the user,
24019 * following the practice Jetpack are using with their theme installations.
24020 *
24021 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
24022 *
24023 * @author Vova Feldman (@svovaf)
24024 * @since 1.2.1.7
24025 */
24026 function _install_premium_version_ajax_action() {
24027 $this->_logger->entrance();
24028
24029 $this->check_ajax_referer( 'install_premium_version' );
24030
24031 if ( ! $this->is_registered() ) {
24032 // Not registered.
24033 self::shoot_ajax_failure( array(
24034 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
24035 'code' => 'premium_installed',
24036 ) );
24037 }
24038
24039 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
24040
24041 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24042 // Invalid ID.
24043 self::shoot_ajax_failure( array(
24044 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24045 'code' => 'invalid_module_id',
24046 ) );
24047 }
24048
24049 if ( $plugin_id == $this->get_id() ) {
24050 if ( $this->is_premium() ) {
24051 // Already using the premium code version.
24052 self::shoot_ajax_failure( array(
24053 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
24054 'code' => 'premium_installed',
24055 ) );
24056 }
24057 if ( ! $this->can_use_premium_code() ) {
24058 // Don't have access to the premium code.
24059 self::shoot_ajax_failure( array(
24060 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
24061 'code' => 'invalid_license',
24062 ) );
24063 }
24064 if ( ! $this->has_release_on_freemius() ) {
24065 // Plugin is a serviceware, no premium code version.
24066 self::shoot_ajax_failure( array(
24067 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
24068 'code' => 'premium_version_missing',
24069 ) );
24070 }
24071 } else {
24072 $addon = $this->get_addon( $plugin_id );
24073
24074 if ( ! is_object( $addon ) ) {
24075 // Invalid add-on ID.
24076 self::shoot_ajax_failure( array(
24077 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24078 'code' => 'invalid_module_id',
24079 ) );
24080 }
24081
24082 if ( $this->is_addon_activated( $plugin_id, true ) ) {
24083 // Premium add-on version is already activated.
24084 self::shoot_ajax_failure( array(
24085 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
24086 'code' => 'premium_installed',
24087 ) );
24088 }
24089 }
24090
24091 $this->_isAutoInstall = true;
24092
24093 // Try to install and activate.
24094 $updater = FS_Plugin_Updater::instance( $this );
24095 $result = $updater->install_and_activate_plugin( $plugin_id );
24096
24097 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
24098 self::shoot_ajax_failure( array(
24099 'message' => $result['message'],
24100 'code' => $result['code'],
24101 ) );
24102 }
24103
24104 self::shoot_ajax_success( $result );
24105 }
24106
24107 /**
24108 * Displays module activation dialog box after a successful upgrade
24109 * where the user explicitly requested to auto download and install
24110 * the premium version.
24111 *
24112 * @author Vova Feldman (@svovaf)
24113 * @since 1.2.1.7
24114 */
24115 function _add_auto_installation_dialog_box() {
24116 $this->_logger->entrance();
24117
24118 if ( ! $this->is_registered() ) {
24119 // Not registered.
24120 return;
24121 }
24122
24123 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
24124
24125 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24126 // Invalid module ID.
24127 return;
24128 }
24129
24130 if ( $plugin_id == $this->get_id() ) {
24131 if ( $this->is_premium() ) {
24132 // Already using the premium code version.
24133 return;
24134 }
24135 if ( ! $this->can_use_premium_code() ) {
24136 // Don't have access to the premium code.
24137 return;
24138 }
24139 if ( ! $this->has_release_on_freemius() ) {
24140 // Plugin is a serviceware, no premium code version.
24141 return;
24142 }
24143 } else {
24144 $addon = $this->get_addon( $plugin_id );
24145
24146 if ( ! is_object( $addon ) ) {
24147 // Invalid add-on ID.
24148 return;
24149 }
24150
24151 if ( $this->is_addon_activated( $plugin_id, true ) ) {
24152 // Premium add-on version is already activated.
24153 return;
24154 }
24155 }
24156
24157 $vars = array(
24158 'id' => $this->_module_id,
24159 'target_module_id' => $plugin_id,
24160 'slug' => $this->_slug,
24161 );
24162
24163 fs_require_template( 'auto-installation.php', $vars );
24164 }
24165
24166 #endregion
24167
24168 #--------------------------------------------------------------------------------
24169 #region Tabs Integration
24170 #--------------------------------------------------------------------------------
24171
24172 #region Module's Original Tabs
24173
24174 /**
24175 * Inject a JavaScript logic to capture the theme tabs HTML.
24176 *
24177 * @author Vova Feldman (@svovaf)
24178 * @since 1.2.2.7
24179 */
24180 function _tabs_capture() {
24181 $this->_logger->entrance();
24182
24183 if ( ! $this->is_product_settings_page() ||
24184 ! $this->is_matching_url( $this->main_menu_url() )
24185 ) {
24186 return;
24187 }
24188
24189 $params = array(
24190 'id' => $this->_module_id,
24191 );
24192
24193 fs_require_once_template( 'tabs-capture-js.php', $params );
24194 }
24195
24196 /**
24197 * Cache theme's tabs HTML for a week. The cache will also be set as expired
24198 * after version and type (free/premium) changes, in addition to the week period.
24199 *
24200 * @author Vova Feldman (@svovaf)
24201 * @since 1.2.2.7
24202 */
24203 function _store_tabs_ajax_action() {
24204 $this->_logger->entrance();
24205
24206 $this->check_ajax_referer( 'store_tabs' );
24207
24208 // Init filesystem if not yet initiated.
24209 WP_Filesystem();
24210
24211 // Get POST body HTML data.
24212 global $wp_filesystem;
24213 $tabs_html = $wp_filesystem->get_contents( "php://input" );
24214
24215 if ( is_string( $tabs_html ) ) {
24216 $tabs_html = trim( $tabs_html );
24217 }
24218
24219 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
24220 self::shoot_ajax_failure();
24221 }
24222
24223 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
24224
24225 self::shoot_ajax_success();
24226 }
24227
24228 /**
24229 * Cache theme's settings page custom styles. The cache will also be set as expired
24230 * after version and type (free/premium) changes, in addition to the week period.
24231 *
24232 * @author Vova Feldman (@svovaf)
24233 * @since 1.2.2.7
24234 */
24235 function _store_tabs_styles() {
24236 $this->_logger->entrance();
24237
24238 if ( ! $this->is_product_settings_page() ||
24239 ! $this->is_matching_url( $this->main_menu_url() )
24240 ) {
24241 return;
24242 }
24243
24244 $wp_styles = wp_styles();
24245
24246 $theme_styles_url = get_template_directory_uri();
24247
24248 $stylesheets = array();
24249 foreach ( $wp_styles->queue as $handler ) {
24250 if ( fs_starts_with( $handler, 'fs_' ) ) {
24251 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
24252 continue;
24253 }
24254
24255 /**
24256 * @var _WP_Dependency $stylesheet
24257 */
24258 $stylesheet = $wp_styles->registered[ $handler ];
24259
24260 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
24261 $stylesheets[] = $stylesheet->src;
24262 }
24263 }
24264
24265 if ( ! empty( $stylesheets ) ) {
24266 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
24267 }
24268 }
24269
24270 /**
24271 * Check if module's original settings page has any tabs.
24272 *
24273 * @author Vova Feldman (@svovaf)
24274 * @since 1.2.2.7
24275 *
24276 * @return bool
24277 */
24278 private function has_tabs() {
24279 return $this->_cache->has( 'tabs' );
24280 }
24281
24282 /**
24283 * Get module's settings page HTML content, starting
24284 * from the beginning of the <div class="wrap"> element,
24285 * until the tabs HTML (including).
24286 *
24287 * @author Vova Feldman (@svovaf)
24288 * @since 1.2.2.7
24289 *
24290 * @return string
24291 */
24292 private function get_tabs_html() {
24293 $this->_logger->entrance();
24294
24295 return $this->_cache->get( 'tabs' );
24296 }
24297
24298 /**
24299 * Check if page should include tabs.
24300 *
24301 * @author Vova Feldman (@svovaf)
24302 * @since 1.2.2.7
24303 *
24304 * @return bool
24305 */
24306 private function should_page_include_tabs() {
24307 if ( ! $this->has_settings_menu() ) {
24308 // Don't add tabs if no settings at all.
24309 return false;
24310 }
24311
24312 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
24313 // Only add tabs to themes for now.
24314 return false;
24315 }
24316
24317 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
24318 // Only add tabs to monetizing themes.
24319 return false;
24320 }
24321
24322 if ( ! $this->is_product_settings_page() ) {
24323 // Only add tabs if browsing one of the product's setting pages.
24324 return false;
24325 }
24326
24327 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
24328 // Don't include tabs in the activation page.
24329 return false;
24330 }
24331
24332 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
24333 // Don't add tabs on checkout page, we want to reduce distractions
24334 // as much as possible.
24335 return false;
24336 }
24337
24338 return true;
24339 }
24340
24341 /**
24342 * Add the tabs HTML before the setting's page content and
24343 * enqueue any required stylesheets.
24344 *
24345 * @author Vova Feldman (@svovaf)
24346 * @since 1.2.2.7
24347 *
24348 * @return bool If tabs were included.
24349 */
24350 function _add_tabs_before_content() {
24351 $this->_logger->entrance();
24352
24353 if ( ! $this->should_page_include_tabs() ) {
24354 return false;
24355 }
24356
24357 /**
24358 * Enqueue the original stylesheets that are included in the
24359 * theme settings page. That way, if the theme settings has
24360 * some custom _styled_ content above the tabs UI, this
24361 * will make sure that the styling is preserved.
24362 */
24363 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
24364 if ( is_array( $stylesheets ) ) {
24365 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
24366 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
24367 }
24368 }
24369
24370 // Cut closing </div> tag.
24371 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
24372
24373 return true;
24374 }
24375
24376 /**
24377 * Add the tabs closing HTML after the setting's page content.
24378 *
24379 * @author Vova Feldman (@svovaf)
24380 * @since 1.2.2.7
24381 *
24382 * @return bool If tabs closing HTML was included.
24383 */
24384 function _add_tabs_after_content() {
24385 $this->_logger->entrance();
24386
24387 if ( ! $this->should_page_include_tabs() ) {
24388 return false;
24389 }
24390
24391 echo '</div>';
24392
24393 return true;
24394 }
24395
24396 #endregion
24397
24398 /**
24399 * Add in-page JavaScript to inject the Freemius tabs into
24400 * the module's setting tabs section.
24401 *
24402 * @author Vova Feldman (@svovaf)
24403 * @since 1.2.2.7
24404 */
24405 function _add_freemius_tabs() {
24406 $this->_logger->entrance();
24407
24408 if ( ! $this->should_page_include_tabs() ) {
24409 return;
24410 }
24411
24412 $params = array( 'id' => $this->_module_id );
24413 fs_require_once_template( 'tabs.php', $params );
24414 }
24415
24416 #endregion
24417
24418 #--------------------------------------------------------------------------------
24419 #region Customizer Integration for Themes
24420 #--------------------------------------------------------------------------------
24421
24422 /**
24423 * @author Vova Feldman (@svovaf)
24424 * @since 1.2.2.7
24425 *
24426 * @param WP_Customize_Manager $customizer
24427 */
24428 function _customizer_register( $customizer ) {
24429 $this->_logger->entrance();
24430
24431 if ( $this->is_pricing_page_visible() ) {
24432 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
24433
24434 $customizer->add_section( 'freemius_upsell', array(
24435 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
24436 'priority' => 1,
24437 ) );
24438 $customizer->add_setting( 'freemius_upsell', array(
24439 'sanitize_callback' => 'esc_html',
24440 ) );
24441
24442 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
24443 'fs' => $this,
24444 'section' => 'freemius_upsell',
24445 'priority' => 100,
24446 ) ) );
24447 }
24448
24449 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
24450 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
24451
24452 // Main Documentation Link In Customizer Root.
24453 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
24454 'fs' => $this,
24455 'priority' => 1000,
24456 ) ) );
24457 }
24458 }
24459
24460 #endregion
24461
24462 /**
24463 * If the theme has a paid version, add some custom
24464 * styling to the theme's premium version (if exists)
24465 * to highlight that it's the premium version of the
24466 * same theme, making it easier for identification
24467 * after the user upgrades and upload it to the site.
24468 *
24469 * @author Vova Feldman (@svovaf)
24470 * @since 1.2.2.7
24471 */
24472 function _style_premium_theme() {
24473 $this->_logger->entrance();
24474
24475 if ( ! self::is_themes_page() ) {
24476 // Only include in the themes page.
24477 return;
24478 }
24479
24480 if ( ! $this->has_paid_plan() ) {
24481 // Only include if has any paid plans.
24482 return;
24483 }
24484
24485 $params = null;
24486 fs_require_once_template( '/js/jquery.content-change.php', $params );
24487
24488 $params = array(
24489 'slug' => $this->_slug,
24490 'id' => $this->_module_id,
24491 );
24492
24493 fs_require_template( '/js/style-premium-theme.php', $params );
24494 }
24495
24496 /**
24497 * This method will return the absolute URL of the module's local icon.
24498 *
24499 * When you are running your plugin or theme on a **localhost** environment, if the icon
24500 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
24501 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
24502 * If an icon is found, this method will automatically attempt to download the icon and store it
24503 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
24504 *
24505 * It's important to mention that this method is NOT phoning home since the developer will deploy
24506 * the product with the local icon in the assets folder. The download process just simplifies
24507 * the process for the developer.
24508 *
24509 * @author Vova Feldman (@svovaf)
24510 * @since 2.0.0
24511 *
24512 * @return string
24513 */
24514 function get_local_icon_url() {
24515 global $fs_active_plugins;
24516
24517 /**
24518 * @since 1.1.7.5
24519 */
24520 $local_path = $this->apply_filters( 'plugin_icon', false );
24521
24522 if ( is_string( $local_path ) ) {
24523 $icons = array( $local_path );
24524 } else {
24525 $img_dir = WP_FS__DIR_IMG;
24526
24527 // Locate the main assets folder.
24528 if ( 1 < count( $fs_active_plugins->plugins ) ) {
24529 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
24530
24531 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
24532 if ( $data->plugin_path == $this->get_plugin_basename() ) {
24533 $img_dir = $plugin_or_theme_img_dir
24534 . '/'
24535 /**
24536 * The basename will be `themes` or the basename of a custom themes directory.
24537 *
24538 * @author Leo Fajardo (@leorw)
24539 * @since 2.2.3
24540 */
24541 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
24542 . '/assets/img';
24543
24544 break;
24545 }
24546 }
24547 }
24548
24549 // Try to locate the icon in the assets folder.
24550 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
24551
24552 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
24553 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
24554 $icons = array(
24555 fs_normalize_path( $img_dir . '/theme-icon.png' )
24556 );
24557 } else {
24558 $icon_found = false;
24559 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
24560
24561 if ( ! function_exists( 'get_filesystem_method' ) ) {
24562 require_once ABSPATH . 'wp-admin/includes/file.php';
24563 }
24564
24565 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
24566
24567 /**
24568 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
24569 *
24570 * This code will only be executed once during the testing
24571 * of the plugin in a local environment. The plugin icon file WILL
24572 * already exist in the assets folder when the plugin is deployed to
24573 * the repository.
24574 */
24575 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
24576 // Fetch icon from Freemius.
24577 $icon = $this->fetch_remote_icon_url();
24578
24579 // Fetch icon from WordPress.org.
24580 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
24581 if ( ! function_exists( 'plugins_api' ) ) {
24582 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
24583 }
24584
24585 $plugin_information = plugins_api( 'plugin_information', array(
24586 'slug' => $this->_slug,
24587 'fields' => array(
24588 'sections' => false,
24589 'tags' => false,
24590 'icons' => true
24591 )
24592 ) );
24593
24594 if (
24595 ! is_wp_error( $plugin_information )
24596 && isset( $plugin_information->icons )
24597 && ! empty( $plugin_information->icons )
24598 ) {
24599 /**
24600 * Get the smallest icon.
24601 *
24602 * @author Leo Fajardo (@leorw)
24603 * @since 1.2.2
24604 */
24605 $icon = end( $plugin_information->icons );
24606 }
24607 }
24608
24609 if ( ! empty( $icon ) ) {
24610 if ( 0 !== strpos( $icon, 'http' ) ) {
24611 $icon = 'http:' . $icon;
24612 }
24613
24614 /**
24615 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
24616 *
24617 * @author Leo Fajardo (@leorw)
24618 * @since 1.2.2
24619 */
24620 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
24621
24622 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
24623
24624 // Try to download the icon.
24625 $icon_found = fs_download_image( $icon, $local_path );
24626 }
24627 }
24628
24629 if ( ! $icon_found ) {
24630 // No icons found, fallback to default icon.
24631 if ( $have_write_permissions ) {
24632 // If have write permissions, copy default icon.
24633 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
24634 } else {
24635 // If doesn't have write permissions, use default icon path.
24636 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
24637 }
24638 }
24639
24640 $icons = array( $local_path );
24641 }
24642 }
24643 }
24644
24645 $icon_dir = dirname( $icons[0] );
24646
24647 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
24648 }
24649
24650 /**
24651 * Fetch module's extended info.
24652 *
24653 * @author Vova Feldman (@svovaf)
24654 * @since 2.0.0
24655 *
24656 * @return object|mixed
24657 */
24658 private function fetch_module_info() {
24659 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
24660 }
24661
24662 /**
24663 * Fetch module's remote icon URL.
24664 *
24665 * @author Vova Feldman (@svovaf)
24666 * @since 2.0.0
24667 *
24668 * @return string
24669 */
24670 function fetch_remote_icon_url() {
24671 $info = $this->fetch_module_info();
24672
24673 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
24674 $info->icon :
24675 '';
24676 }
24677
24678 #--------------------------------------------------------------------------------
24679 #region GDPR
24680 #--------------------------------------------------------------------------------
24681
24682 /**
24683 * @author Leo Fajardo (@leorw)
24684 * @since 2.1.0
24685 *
24686 * @return bool
24687 */
24688 function fetch_and_store_current_user_gdpr_anonymously() {
24689 $pong = $this->ping( null, true );
24690
24691 if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
24692 return false;
24693 } else {
24694 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
24695
24696 return $pong->is_gdpr_required;
24697 }
24698 }
24699
24700 /**
24701 * @author Leo Fajardo (@leorw)
24702 * @since 2.1.0
24703 *
24704 * @param array $user_plugins
24705 *
24706 * @return string
24707 */
24708 private function get_gdpr_admin_notice_string( $user_plugins ) {
24709 $this->_logger->entrance();
24710
24711 $addons = self::get_all_addons();
24712
24713 foreach ( $user_plugins as $user_plugin ) {
24714 $has_addons = isset( $addons[ $user_plugin->id ] );
24715
24716 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
24717 if ( $this->_module_id == $user_plugin->id ) {
24718 $addons = $this->get_addons();
24719 $has_addons = ( ! empty( $addons ) );
24720 } else {
24721 $plugin_api = FS_Api::instance(
24722 $user_plugin->id,
24723 'plugin',
24724 $user_plugin->id,
24725 $user_plugin->public_key,
24726 ! $user_plugin->is_live,
24727 false,
24728 $this->get_sdk_version()
24729 );
24730
24731 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
24732
24733 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
24734 is_array( $addons_result->plugins ) &&
24735 ! empty( $addons_result->plugins )
24736 ) {
24737 $has_addons = true;
24738 }
24739 }
24740 }
24741
24742 $user_plugin->has_addons = $has_addons;
24743 }
24744
24745 $is_single_parent_product = ( 1 === count( $user_plugins ) );
24746
24747 $multiple_products_text = '';
24748
24749 if ( $is_single_parent_product ) {
24750 $single_parent_product = reset( $user_plugins );
24751
24752 $thank_you = sprintf(
24753 "<span data-plugin-id='%d'>%s</span>",
24754 $single_parent_product->id,
24755 sprintf(
24756 $single_parent_product->has_addons ?
24757 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
24758 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
24759 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
24760 )
24761 );
24762
24763 $already_opted_in = sprintf(
24764 $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' ),
24765 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
24766 );
24767 } else {
24768 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
24769 $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' );
24770
24771 $products_and_add_ons = '';
24772 foreach ( $user_plugins as $user_plugin ) {
24773 if ( ! empty( $products_and_add_ons ) ) {
24774 $products_and_add_ons .= ', ';
24775 }
24776
24777 if ( ! $user_plugin->has_addons ) {
24778 $products_and_add_ons .= sprintf(
24779 "<span data-plugin-id='%d'>%s</span>",
24780 $user_plugin->id,
24781 $user_plugin->title
24782 );
24783 } else {
24784 $products_and_add_ons .= sprintf(
24785 "<span data-plugin-id='%d'>%s</span>",
24786 $user_plugin->id,
24787 sprintf(
24788 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
24789 $user_plugin->title
24790 )
24791 );
24792 }
24793 }
24794
24795 $multiple_products_text = sprintf(
24796 "<small class='products'><strong>%s:</strong> %s</small>",
24797 $this->get_text_inline( 'Products', 'products' ),
24798 $products_and_add_ons
24799 );
24800 }
24801
24802 $actions = sprintf(
24803 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
24804 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
24805 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
24806 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
24807 sprintf(
24808 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
24809 '<span class="underlined">',
24810 '</span>'
24811 )
24812 );
24813
24814 return sprintf(
24815 '%s %s %s',
24816 $thank_you,
24817 $already_opted_in,
24818 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>' ) .
24819 '<br><br>' .
24820 '<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>' .
24821 $actions .
24822 ( $is_single_parent_product ? '' : $multiple_products_text )
24823 );
24824 }
24825
24826 /**
24827 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
24828 * plugins and themes they've opted in to.
24829 *
24830 * @author Leo Fajardo (@leorw)
24831 * @since 2.1.0
24832 *
24833 * @param string $user_email
24834 * @param string $license_key
24835 * @param array $plugin_ids
24836 * @param string|null $license_key
24837 *
24838 * @return array|false
24839 */
24840 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
24841 $request = array(
24842 'method' => 'POST',
24843 'body' => array(),
24844 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
24845 );
24846
24847 if ( is_string( $user_email ) ) {
24848 $request['body']['email'] = $user_email;
24849 } else {
24850 $request['body']['license_key'] = $license_key;
24851 }
24852
24853 $result = array();
24854
24855 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
24856 $total_plugin_ids = count( $plugin_ids );
24857
24858 $plugin_ids_count_per_request = 10;
24859 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
24860 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
24861
24862 $request['body']['plugin_ids'] = $plugin_ids_set;
24863
24864 $response = self::safe_remote_post(
24865 $url,
24866 $request,
24867 WP_FS__TIME_24_HOURS_IN_SEC,
24868 WP_FS__TIME_12_HOURS_IN_SEC
24869 );
24870
24871 if ( ! is_wp_error( $response ) ) {
24872 $decoded = is_string( $response['body'] ) ?
24873 json_decode( $response['body'] ) :
24874 null;
24875
24876 if (
24877 !is_object($decoded) ||
24878 !isset($decoded->success) ||
24879 true !== $decoded->success ||
24880 !isset( $decoded->data ) ||
24881 !is_array( $decoded->data )
24882 ) {
24883 return false;
24884 }
24885
24886 $result = array_merge( $result, $decoded->data );
24887 }
24888 }
24889
24890 return $result;
24891 }
24892
24893 /**
24894 * @author Leo Fajardo (@leorw)
24895 * @since 2.1.0
24896 */
24897 function _maybe_show_gdpr_admin_notice() {
24898 if ( ! $this->is_user_in_admin() ) {
24899 return;
24900 }
24901
24902 if ( ! $this->should_handle_gdpr_admin_notice() ) {
24903 return;
24904 }
24905
24906 if ( ! $this->is_user_admin() ) {
24907 return;
24908 }
24909
24910 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
24911
24912 $lock = FS_User_Lock::instance();
24913
24914 /**
24915 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
24916 */
24917 if ( ! $lock->try_lock( 60 ) ) {
24918 return;
24919 }
24920
24921 /**
24922 * @var $current_wp_user WP_User
24923 */
24924 $current_wp_user = self::_get_current_wp_user();
24925
24926 /**
24927 * @var FS_User $current_fs_user
24928 */
24929 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
24930
24931 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
24932
24933 if ( ! is_object( $current_fs_user ) ) {
24934 // 10-year lock.
24935 $lock->lock( $ten_years_in_sec );
24936
24937 return;
24938 }
24939
24940 $gdpr = FS_GDPR_Manager::instance();
24941
24942 if ( $gdpr->is_opt_in_notice_shown() ) {
24943 // 30-day lock.
24944 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
24945
24946 return;
24947 }
24948
24949 if ( ! $gdpr->should_show_opt_in_notice() ) {
24950 // 10-year lock.
24951 $lock->lock( $ten_years_in_sec );
24952
24953 return;
24954 }
24955
24956 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
24957 $was_notice_shown_before = ( false !== $last_time_notice_shown );
24958
24959 if ( $was_notice_shown_before &&
24960 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
24961 ) {
24962 // If the notice was shown before, show it again after 30 days from the last time it was shown.
24963 return;
24964 }
24965
24966 /**
24967 * Find all plugin IDs that were installed by the current admin.
24968 */
24969 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
24970
24971 if ( empty( $plugin_ids_map )) {
24972 $lock->lock( $ten_years_in_sec );
24973
24974 return;
24975 }
24976
24977 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
24978 $current_fs_user->email,
24979 null,
24980 array_keys( $plugin_ids_map )
24981 );
24982
24983 if ( empty( $user_plugins ) ) {
24984 $lock->lock(
24985 is_array($user_plugins) ?
24986 $ten_years_in_sec :
24987 // Lock for 24-hours on errors.
24988 WP_FS__TIME_24_HOURS_IN_SEC
24989 );
24990
24991 return;
24992 }
24993
24994 $has_unset_marketing_optin = false;
24995
24996 foreach ( $user_plugins as $user_plugin ) {
24997 if ( true == $user_plugin->is_marketing_allowed ) {
24998 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
24999 }
25000
25001 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
25002 $has_unset_marketing_optin = true;
25003 }
25004 }
25005
25006 if ( empty( $plugin_ids_map ) ||
25007 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
25008 ) {
25009 $lock->lock( $ten_years_in_sec );
25010
25011 return;
25012 }
25013
25014 $modules = array_merge(
25015 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25016 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25017 );
25018
25019 foreach ( $modules as $module ) {
25020 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
25021 $plugin_ids_map[ $module->id ] = $module;
25022 }
25023 }
25024
25025 $plugin_title = null;
25026 if ( 1 === count( $plugin_ids_map ) ) {
25027 $module = reset( $plugin_ids_map );
25028 $plugin_title = $module->title;
25029 }
25030
25031 $gdpr->add_opt_in_sticky_notice(
25032 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
25033 $plugin_title
25034 );
25035
25036 $this->add_gdpr_optin_ajax_handler_and_style();
25037
25038 $gdpr->notice_was_just_shown();
25039
25040 // 30-day lock.
25041 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25042 }
25043
25044 /**
25045 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
25046 * marketing.
25047 *
25048 * @author Leo Fajardo (@leorw)
25049 * @since 2.1.0
25050 */
25051 private function disable_opt_in_notice_and_lock_user() {
25052 FS_GDPR_Manager::instance()->disable_opt_in_notice();
25053
25054 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25055
25056 // 10-year lock.
25057 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
25058 }
25059
25060 /**
25061 * @author Leo Fajardo (@leorw)
25062 * @since 2.1.0
25063 */
25064 function _add_gdpr_optin_js() {
25065 $vars = array( 'id' => $this->_module_id );
25066
25067 fs_require_once_template( 'gdpr-optin-js.php', $vars );
25068 }
25069
25070 /**
25071 * @author Leo Fajardo (@leorw)
25072 * @since 2.1.0
25073 */
25074 function enqueue_gdpr_optin_notice_style() {
25075 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
25076 }
25077
25078 /**
25079 * @author Leo Fajardo (@leorw)
25080 * @since 2.1.0
25081 */
25082 function _maybe_add_gdpr_optin_ajax_handler() {
25083 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
25084
25085 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
25086 $this->add_gdpr_optin_ajax_handler_and_style();
25087 }
25088 }
25089
25090 /**
25091 * @author Leo Fajardo (@leorw)
25092 * @since 2.1.0
25093 */
25094 function _fetch_is_marketing_required_flag_value_ajax_action() {
25095 $this->_logger->entrance();
25096
25097 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
25098
25099 $license_key = fs_request_get( 'license_key' );
25100
25101 if ( empty($license_key) ) {
25102 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
25103 }
25104
25105 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25106 null,
25107 $license_key,
25108 array( $this->_module_id )
25109 );
25110
25111 if ( ! is_array( $user_plugins ) ||
25112 empty($user_plugins) ||
25113 !isset($user_plugins[0]->plugin_id) ||
25114 $user_plugins[0]->plugin_id != $this->_module_id
25115 ) {
25116 /**
25117 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
25118 *
25119 * @author Vova Feldman (@svovaf)
25120 */
25121 self::shoot_ajax_success( array(
25122 'is_marketing_allowed' => null,
25123 'license_owner_id' => null
25124 ) );
25125 }
25126
25127 self::shoot_ajax_success( array(
25128 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
25129 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
25130 ) );
25131 }
25132
25133 /**
25134 * @author Leo Fajardo (@leorw)
25135 * @since 2.3.2
25136 *
25137 * @param number[] $install_ids
25138 *
25139 * @return array {
25140 * An array of objects containing the installs' licenses owners data.
25141 *
25142 * @property number $id User ID.
25143 * @property string $email User email (can be masked email).
25144 * }
25145 */
25146 private function fetch_installs_licenses_owners_data( $install_ids ) {
25147 $this->_logger->entrance();
25148
25149 $response = $this->get_api_user_scope()->get(
25150 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
25151 );
25152
25153 $license_owners = null;
25154
25155 if ( $this->is_api_result_object( $response, 'owners' ) ) {
25156 $license_owners = $response->owners;
25157 }
25158
25159 return $license_owners;
25160 }
25161
25162 /**
25163 * @author Leo Fajardo (@leorw)
25164 * @since 2.1.0
25165 */
25166 private function add_gdpr_optin_ajax_handler_and_style() {
25167 // Add GDPR action AJAX callback.
25168 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
25169
25170 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
25171 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
25172 }
25173
25174 /**
25175 * @author Leo Fajardo (@leorw)
25176 * @since 2.1.0
25177 */
25178 function _gdpr_optin_ajax_action() {
25179 $this->_logger->entrance();
25180
25181 $this->check_ajax_referer( 'gdpr_optin_action' );
25182
25183 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
25184 self::shoot_ajax_failure();
25185 }
25186
25187 $current_wp_user = self::_get_current_wp_user();
25188
25189 $plugin_ids = fs_request_get( 'plugin_ids', array() );
25190 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
25191 self::shoot_ajax_failure();
25192 }
25193
25194 $modules = array_merge(
25195 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25196 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25197 );
25198
25199 foreach ( $modules as $key => $module ) {
25200 if ( ! in_array( $module->id, $plugin_ids ) ) {
25201 unset( $modules[ $key ] );
25202 }
25203 }
25204
25205 if ( empty( $modules ) ) {
25206 self::shoot_ajax_failure();
25207 }
25208
25209 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
25210
25211 foreach ( $modules as $module ) {
25212 $user_api->call( "?plugin_id={$module->id}", 'put', array(
25213 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
25214 ) );
25215 }
25216
25217 FS_GDPR_Manager::instance()->remove_opt_in_notice();
25218
25219 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25220
25221 // 10-year lock.
25222 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
25223
25224 self::shoot_ajax_success();
25225 }
25226
25227 /**
25228 * 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.
25229 *
25230 * @author Vova Feldman (@svovaf)
25231 * @since 2.1.0
25232 *
25233 * @return bool
25234 */
25235 private function should_handle_gdpr_admin_notice() {
25236 return $this->apply_filters(
25237 'handle_gdpr_admin_notice',
25238 // Default to false.
25239 false
25240 );
25241 }
25242
25243 #endregion
25244
25245 #----------------------------------------------------------------------------------
25246 #region Marketing
25247 #----------------------------------------------------------------------------------
25248
25249 /**
25250 * Check if current user purchased any other plugins before.
25251 *
25252 * @author Vova Feldman (@svovaf)
25253 * @since 1.0.9
25254 *
25255 * @return bool
25256 */
25257 function has_purchased_before() {
25258 // TODO: Implement has_purchased_before() method.
25259 throw new Exception( 'not implemented' );
25260 }
25261
25262 /**
25263 * Check if current user classified as an agency.
25264 *
25265 * @author Vova Feldman (@svovaf)
25266 * @since 1.0.9
25267 *
25268 * @return bool
25269 */
25270 function is_agency() {
25271 // TODO: Implement is_agency() method.
25272 throw new Exception( 'not implemented' );
25273 }
25274
25275 /**
25276 * Check if current user classified as a developer.
25277 *
25278 * @author Vova Feldman (@svovaf)
25279 * @since 1.0.9
25280 *
25281 * @return bool
25282 */
25283 function is_developer() {
25284 // TODO: Implement is_developer() method.
25285 throw new Exception( 'not implemented' );
25286 }
25287
25288 /**
25289 * Check if current user classified as a business.
25290 *
25291 * @author Vova Feldman (@svovaf)
25292 * @since 1.0.9
25293 *
25294 * @return bool
25295 */
25296 function is_business() {
25297 // TODO: Implement is_business() method.
25298 throw new Exception( 'not implemented' );
25299 }
25300
25301 #endregion
25302
25303 #----------------------------------------------------------------------------------
25304 #region Helper
25305 #----------------------------------------------------------------------------------
25306
25307 /**
25308 * If running with a secret key, assume it's the developer and show pending plans as well.
25309 *
25310 * @author Vova Feldman (@svovaf)
25311 * @since 2.1.2
25312 *
25313 * @param string $path
25314 *
25315 * @return string
25316 */
25317 function add_show_pending( $path ) {
25318 if ( ! $this->has_secret_key() ) {
25319 return $path;
25320 }
25321
25322 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
25323 }
25324
25325 #endregion
25326 }
25327