PluginProbe
Passster – Password Protect Pages and Content / 4.2.11
Passster – Password Protect Pages and Content v4.2.11
4.3.16 4.3.15 4.3.14 4.3.12 4.3.13 4.3.11 4.3.10 4.3.9 4.3.8 4.3.7 4.3.6 4.3.5 trunk 3.5.4 3.5.5.2 3.5.5.8 3.5.5.9 4.0 4.1.4 4.2.10 4.2.11 4.2.12 4.2.13 4.2.14 4.2.15 All 48 releases
content-protector / inc / freemius / includes / class-freemius.php

class-freemius.php in Passster – Password Protect Pages and Content 4.2.11, at inc/freemius/includes/class-freemius.php

26,126 lines 983.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 1.1.7.5
114 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
115 */
116 private $_anonymous_mode;
117
118 /**
119 * @since 1.1.9
120 * @var bool Hints the SDK if plugin have any free plans.
121 */
122 private $_is_premium_only;
123
124 /**
125 * @since 1.2.1.6
126 * @var bool Hints the SDK if plugin have premium code version at all.
127 */
128 private $_has_premium_version;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
133 */
134 private $_ignore_pending_mode;
135
136 /**
137 * @since 1.0.8
138 * @var bool Hints the SDK if the plugin has any paid plans.
139 */
140 private $_has_paid_plans;
141
142 /**
143 * @since 1.2.1.5
144 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
145 * without a specified period, if positive - the number of trial days.
146 */
147 private $_trial_days = - 1;
148
149 /**
150 * @since 1.2.1.5
151 * @var bool Hints the SDK if the trial requires a payment method or not.
152 */
153 private $_is_trial_require_payment = false;
154
155 /**
156 * @since 1.0.7
157 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
158 */
159 private $_is_org_compliant;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is has add-ons.
164 */
165 private $_has_addons;
166
167 /**
168 * @since 2.4.5
169 * @var string Navigation type: 'menu' or 'tabs'.
170 */
171 private $_navigation;
172
173 const NAVIGATION_MENU = 'menu';
174 const NAVIGATION_TABS = 'tabs';
175
176 /**
177 * @since 1.1.6
178 * @var string[]bool.
179 */
180 private $_permissions;
181
182 /**
183 * @var FS_Storage
184 */
185 private $_storage;
186
187 /**
188 * @since 1.2.2.7
189 * @var FS_Cache_Manager
190 */
191 private $_cache;
192
193 /**
194 * @since 1.0.0
195 *
196 * @var FS_Logger
197 */
198 private $_logger;
199 /**
200 * @since 1.0.4
201 *
202 * @var FS_Plugin
203 */
204 private $_plugin = false;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin|false
209 */
210 private $_parent_plugin = false;
211 /**
212 * @since 1.1.1
213 *
214 * @var Freemius
215 */
216 private $_parent = false;
217 /**
218 * @since 1.0.1
219 *
220 * @var FS_User
221 */
222 private $_user = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_Site
227 */
228 private $_site = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Plugin_License
233 */
234 private $_license;
235 /**
236 * @since 1.0.2
237 *
238 * @var FS_Plugin_Plan[]
239 */
240 private $_plans = false;
241 /**
242 * @var FS_Plugin_License[]
243 * @since 1.0.5
244 */
245 private $_licenses = false;
246
247 /**
248 * @since 1.0.1
249 *
250 * @var FS_Admin_Menu_Manager
251 */
252 private $_menu;
253
254 /**
255 * @var FS_Admin_Notices
256 */
257 private $_admin_notices;
258
259 /**
260 * @since 1.1.6
261 *
262 * @var FS_Admin_Notices
263 */
264 private static $_global_admin_notices;
265
266 /**
267 * @var FS_Logger
268 * @since 1.0.0
269 */
270 private static $_static_logger;
271
272 /**
273 * @var FS_Options
274 * @since 1.0.2
275 */
276 private static $_accounts;
277
278 /**
279 * @since 1.2.2
280 *
281 * @var number
282 */
283 private $_module_id;
284
285 /**
286 * @var Freemius[]
287 */
288 private static $_instances = array();
289
290 /**
291 * @since 1.2.3
292 *
293 * @var FS_Affiliate
294 */
295 private $affiliate = null;
296
297 /**
298 * @since 1.2.3
299 *
300 * @var FS_AffiliateTerms
301 */
302 private $plugin_affiliate_terms = null;
303
304 /**
305 * @since 1.2.3
306 *
307 * @var FS_AffiliateTerms
308 */
309 private $custom_affiliate_terms = null;
310
311 /**
312 * @since 2.0.0
313 *
314 * @var bool
315 */
316 private $_is_multisite_integrated;
317
318 /**
319 * @since 2.0.0
320 *
321 * @var bool True if the current request is for a network admin screen and the plugin is network active.
322 */
323 private $_is_network_active;
324
325 /**
326 * @since 2.0.0
327 *
328 * @var int|null The original blog ID the plugin was loaded with.
329 */
330 private $_blog_id = null;
331
332 /**
333 * @since 2.0.0
334 *
335 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
336 */
337 private $_context_is_network_or_blog_id = null;
338
339 /**
340 * @since 2.0.0
341 *
342 * @var string
343 */
344 private $_dynamically_added_top_level_page_hook_name = '';
345
346 /**
347 * @author Leo Fajardo (@leorw)
348 * @since 2.3.1
349 *
350 * @var bool
351 */
352 private $is_whitelabeled;
353
354 /**
355 * @author Leo Fajardo (@leorw)
356 * @since 2.4.0
357 *
358 * @var bool
359 */
360 private $_is_bundle_license_auto_activation_enabled = false;
361
362 #region Uninstall Reasons IDs
363
364 const REASON_NO_LONGER_NEEDED = 1;
365 const REASON_FOUND_A_BETTER_PLUGIN = 2;
366 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
367 const REASON_BROKE_MY_SITE = 4;
368 const REASON_SUDDENLY_STOPPED_WORKING = 5;
369 const REASON_CANT_PAY_ANYMORE = 6;
370 const REASON_OTHER = 7;
371 const REASON_DIDNT_WORK = 8;
372 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
373 const REASON_COULDNT_MAKE_IT_WORK = 10;
374 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
375 const REASON_NOT_WORKING = 12;
376 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
377 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
378 const REASON_TEMPORARY_DEACTIVATION = 15;
379
380 #endregion
381
382 /**
383 * @author Leo Fajardo (@leorw)
384 * @since 2.4.2
385 *
386 * @var string|null
387 */
388 private $_pricing_js_path = null;
389
390 const VERSION_MAX_CHARS = 16;
391 const LANGUAGE_MAX_CHARS = 8;
392
393 /* Ctor
394 ------------------------------------------------------------------------------------------------------------------*/
395
396 /**
397 * Main singleton instance.
398 *
399 * @author Vova Feldman (@svovaf)
400 * @since 1.0.0
401 *
402 * @param number $module_id
403 * @param string|bool $slug
404 * @param bool $is_init Since 1.2.1 Is initiation sequence.
405 */
406 private function __construct( $module_id, $slug = false, $is_init = false ) {
407 $main_file = false;
408
409 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
410 $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
411 }
412
413 $this->_module_id = $module_id;
414 $this->_slug = $this->get_slug();
415 $this->_module_type = $this->get_module_type();
416
417 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
418
419 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
420
421 // If not set or 24 hours have already passed from the last time it's set, set the last load timestamp to the current time.
422 if (
423 ! isset( $this->_storage->last_load_timestamp ) ||
424 $this->_storage->last_load_timestamp < ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC ) )
425 ) {
426 $this->_storage->last_load_timestamp = time();
427 }
428
429 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
430
431 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
432
433 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
434 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
435 $this->_plugin_basename = $this->get_plugin_basename();
436 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
437
438 $this->_is_multisite_integrated = (
439 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
440 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
441 );
442
443 $this->_is_network_active = (
444 is_multisite() &&
445 $this->_is_multisite_integrated &&
446 // Themes are always network activated, but the ACTUAL activation is per site.
447 $this->is_plugin() &&
448 (
449 is_plugin_active_for_network( $this->_plugin_basename ) ||
450 // Plugin network level activation or uninstall.
451 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
452 )
453 );
454
455 $this->_storage->set_network_active(
456 $this->_is_network_active,
457 $this->is_delegated_connection()
458 );
459
460 if ( ! isset( $this->_storage->is_network_activated ) ) {
461 $this->_storage->is_network_activated = $this->_is_network_active;
462 }
463
464 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
465 // Update last activation level.
466 $this->_storage->is_network_activated = $this->_is_network_active;
467
468 $this->maybe_adjust_storage();
469 }
470
471 #region Migration
472
473 if ( is_multisite() ) {
474 /**
475 * If the install_timestamp exists on the site level but doesn't exist on the
476 * network level storage, it means that we need to process the storage with migration.
477 *
478 * 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.
479 *
480 * @author Vova Feldman (@svovaf)
481 * @since 2.0.0
482 */
483 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
484 false !== $this->_storage->get( 'install_timestamp', false, false )
485 ) {
486 // Initiate storage migration.
487 $this->_storage->migrate_to_network();
488
489 // Migrate module cache to network level storage.
490 $this->_cache->migrate_to_network();
491 }
492 }
493
494 #endregion
495
496 $base_name_split = explode( '/', $this->_plugin_basename );
497 $this->_plugin_dir_name = $base_name_split[0];
498
499 if ( $this->_logger->is_on() ) {
500 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
501 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
502 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
503 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
504 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
505 }
506
507 // Remember link between file to slug.
508 $this->store_file_slug_map();
509
510 // Store plugin's initial install timestamp.
511 if ( ! isset( $this->_storage->install_timestamp ) ) {
512 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
513 }
514
515 if ( ! is_object( $this->_plugin ) ) {
516 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
517 }
518
519 $this->_admin_notices = FS_Admin_Notices::instance(
520 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
521 /**
522 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
523 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
524 *
525 * @author Leo Fajardo (@leorw)
526 * @since 1.2.2
527 */
528 ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
529 $this->_plugin->title :
530 $this->get_plugin_name()
531 ),
532 $this->get_unique_affix()
533 );
534
535 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
536 fs_request_is_action( 'restart_freemius' )
537 ) {
538 FS_Api::clear_cache();
539 $this->_cache->clear();
540 }
541
542 $this->register_constructor_hooks();
543
544 /**
545 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
546 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
547 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
548 *
549 * @author Leo Fajardo (@leorw)
550 */
551 self::migrate_install_plan_to_plan_id( $this->_storage );
552
553 $this->_load_account();
554
555 $this->_version_updates_handler();
556 }
557
558 /**
559 * @author Leo Fajardo (@leorw)
560 * @since 2.3.0
561 */
562 private function maybe_adjust_storage() {
563 $install_timestamp = null;
564 $prev_is_premium = null;
565
566 $options_to_update = array();
567
568 $is_network_admin = fs_is_network_admin();
569
570 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
571
572 if ( ! $is_network_admin ) {
573 if ( is_null( $network_install_timestamp ) ) {
574 // Plugin was not network-activated before.
575 return;
576 }
577
578 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
579 // Set the `install_timestamp` only if it's not yet set.
580 $install_timestamp = $network_install_timestamp;
581 }
582
583 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
584 } else {
585 $current_wp_user = self::_get_current_wp_user();
586 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
587 $network_user_info = array();
588
589 $skips_count = 0;
590
591 $sites = self::get_sites();
592 $sites_count = count( $sites );
593
594 $blog_id_2_install_map = array();
595
596 $is_first_non_ignored_blog = true;
597
598 foreach ( $sites as $site ) {
599 $blog_id = self::get_site_blog_id( $site );
600
601 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
602
603 if ( is_null( $blog_install_timestamp ) ) {
604 // Plugin has not been installed on this blog.
605 continue;
606 }
607
608 $is_earlier_install = (
609 ! is_null( $install_timestamp ) &&
610 $blog_install_timestamp < $install_timestamp
611 );
612
613 $install = $this->get_install_by_blog_id( $blog_id );
614
615 $update_network_user_info = false;
616
617 if ( ! is_object( $install ) ) {
618 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
619 // The opt-in decision (whether to skip or opt in) is yet to be made.
620 continue;
621 }
622
623 $skips_count ++;
624 } else {
625 $blog_id_2_install_map[ $blog_id ] = $install;
626
627 if ( empty( $network_user_info ) ) {
628 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
629 $update_network_user_info = true;
630 }
631
632 if ( ! $update_network_user_info &&
633 is_object( $current_fs_user ) &&
634 $network_user_info['user_id'] != $current_fs_user->id &&
635 $install->user_id == $current_fs_user->id
636 ) {
637 // If an install that is owned by the current WP user is found, use its user information instead.
638 $update_network_user_info = true;
639 }
640
641 if ( ! $update_network_user_info &&
642 $is_earlier_install &&
643 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
644 ) {
645 // 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.
646 $update_network_user_info = true;
647 }
648 }
649
650 if ( $update_network_user_info ) {
651 $network_user_info = array(
652 'user_id' => $install->user_id,
653 'blog_id' => $blog_id
654 );
655 }
656
657 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
658
659 if ( $is_first_non_ignored_blog ) {
660 $prev_is_premium = $site_prev_is_premium;
661
662 if ( is_null( $network_install_timestamp ) ) {
663 $install_timestamp = $blog_install_timestamp;
664 }
665
666 $is_first_non_ignored_blog = false;
667
668 continue;
669 }
670
671 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
672 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
673 $prev_is_premium = null;
674 }
675
676 if ( $is_earlier_install ) {
677 // If an earlier install timestamp is found.
678 $install_timestamp = $blog_install_timestamp;
679 }
680 }
681
682 $installs_count = count( $blog_id_2_install_map );
683
684 if ( $sites_count === ( $installs_count + $skips_count ) ) {
685 if ( ! empty( $network_user_info ) ) {
686 $options_to_update['network_user_id'] = $network_user_info['user_id'];
687 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
688
689 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
690 if ( $install->user_id == $network_user_info['user_id'] ) {
691 continue;
692 }
693
694 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
695 }
696 }
697
698 if ( $sites_count === $skips_count ) {
699 /**
700 * Assume network-level skipping as the intended action if all actions identified were only
701 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
702 */
703 $options_to_update['is_anonymous_ms'] = true;
704 } else if ( $sites_count === $installs_count ) {
705 /**
706 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
707 * (i.e., no delegation and skipping of the connections so far).
708 */
709 $options_to_update['is_network_connected'] = true;
710 }
711 }
712 }
713
714 if ( ! is_null( $install_timestamp ) ) {
715 $options_to_update['install_timestamp'] = $install_timestamp;
716 }
717
718 if ( ! is_null( $prev_is_premium ) ) {
719 $options_to_update['prev_is_premium'] = $prev_is_premium;
720 }
721
722 if ( ! empty( $options_to_update ) ) {
723 $this->adjust_storage( $options_to_update, $is_network_admin );
724 }
725 }
726
727 /**
728 * @author Leo Fajardo (@leorw)
729 * @since 2.3.0
730 *
731 * @param array $options
732 * @param bool $is_network_admin
733 */
734 private function adjust_storage( $options, $is_network_admin ) {
735 foreach ( $options as $name => $value ) {
736 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
737 }
738 }
739
740 /**
741 * Checks whether this module has a settings menu.
742 *
743 * @author Leo Fajardo (@leorw)
744 * @since 1.2.2
745 *
746 * @return bool
747 */
748 function has_settings_menu() {
749 return ( $this->_is_network_active && fs_is_network_admin() ) ?
750 $this->_menu->has_network_menu() :
751 $this->_menu->has_menu();
752 }
753
754 /**
755 * 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.
756 *
757 * @author Vova Feldman (@svovaf)
758 * @since 2.4.5
759 *
760 * @return bool
761 */
762 function show_opt_in_on_themes_page() {
763 if ( ! $this->is_free_wp_org_theme() ) {
764 return false;
765 }
766
767 if ( ! $this->has_settings_menu() ) {
768 return true;
769 }
770
771 return $this->show_settings_with_tabs();
772 }
773
774 /**
775 * If `true` the opt-in should be shown on the product's main setting page.
776 *
777 * @author Vova Feldman (@svovaf)
778 * @since 2.4.5
779 *
780 * @return bool
781 *
782 * @uses show_opt_in_on_themes_page();
783 */
784 function show_opt_in_on_setting_page() {
785 return ! $this->show_opt_in_on_themes_page();
786 }
787
788 /**
789 * If `true` the settings should be shown using tabs.
790 *
791 * @author Vova Feldman (@svovaf)
792 * @since 2.4.5
793 *
794 * @return bool
795 */
796 function show_settings_with_tabs() {
797 return ( self::NAVIGATION_TABS === $this->_navigation );
798 }
799
800 /**
801 * Check if the context module is free wp.org theme.
802 *
803 * This method is helpful because:
804 * 1. wp.org themes are limited to a single submenu item,
805 * and sub-submenu items are most likely not allowed (never verified).
806 * 2. wp.org themes are not allowed to redirect the user
807 * after the theme activation, therefore, the agreed UX
808 * is showing the opt-in as a modal dialog box after
809 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
810 *
811 * @author Vova Feldman (@svovaf)
812 * @since 1.2.2.7
813 *
814 * @return bool
815 */
816 function is_free_wp_org_theme() {
817 return (
818 $this->is_theme() &&
819 $this->is_org_repo_compliant() &&
820 ! $this->is_premium()
821 );
822 }
823
824 /**
825 * Checks whether this a submenu item is visible.
826 *
827 * @author Vova Feldman (@svovaf)
828 * @since 1.2.2.6
829 * @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.
830 *
831 * @param string $slug
832 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
833 *
834 * @return bool
835 */
836 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
837 if ( $this->is_admin_page( $slug ) ) {
838 /**
839 * It is the current context page, so show the submenu item
840 * so the user will have the right context page, even if it
841 * was set to hidden.
842 */
843 return true;
844 }
845
846 if ( ! $this->has_settings_menu() ) {
847 // No menu settings at all.
848 return false;
849 }
850
851 if (
852 ! $is_tabs_visibility_check &&
853 $this->is_org_repo_compliant() &&
854 $this->show_settings_with_tabs()
855 ) {
856 /**
857 * wp.org themes are limited to a single submenu item, and
858 * sub-submenu items are most likely not allowed (never verified).
859 */
860 return false;
861 }
862
863 return $this->_menu->is_submenu_item_visible( $slug );
864 }
865
866 /**
867 * Check if a Freemius page should be accessible via the UI.
868 *
869 * @author Vova Feldman (@svovaf)
870 * @since 1.2.2.7
871 *
872 * @param string $slug
873 *
874 * @return bool
875 */
876 function is_page_visible( $slug ) {
877 if ( $this->is_admin_page( $slug ) ) {
878 return true;
879 }
880
881 return $this->_menu->is_submenu_item_visible( $slug, true, true );
882 }
883
884 /**
885 * @author Vova Feldman (@svovaf)
886 * @since 1.0.9
887 */
888 private function _version_updates_handler() {
889 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
890 // Freemius version upgrade mode.
891 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
892 $this->_storage->sdk_version = $this->version;
893
894 if ( empty( $this->_storage->sdk_last_version ) ||
895 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
896 ) {
897 $this->_storage->sdk_upgrade_mode = true;
898 $this->_storage->sdk_downgrade_mode = false;
899 } else {
900 $this->_storage->sdk_downgrade_mode = true;
901 $this->_storage->sdk_upgrade_mode = false;
902
903 }
904
905 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
906 }
907
908 $plugin_version = $this->get_plugin_version();
909 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
910 // Plugin version upgrade mode.
911 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
912 $this->_storage->plugin_version = $plugin_version;
913
914 if ( empty( $this->_storage->plugin_last_version ) ||
915 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
916 ) {
917 $this->_storage->plugin_upgrade_mode = true;
918 $this->_storage->plugin_downgrade_mode = false;
919 } else {
920 $this->_storage->plugin_downgrade_mode = true;
921 $this->_storage->plugin_upgrade_mode = false;
922 }
923
924 if ( ! empty( $this->_storage->plugin_last_version ) ) {
925 // Different version of the plugin was installed before, therefore it's an update.
926 $this->_storage->is_plugin_new_install = false;
927 }
928
929 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
930 }
931 }
932
933 #--------------------------------------------------------------------------------
934 #region Data Migration on SDK Update
935 #--------------------------------------------------------------------------------
936
937 /**
938 * @author Vova Feldman (@svovaf)
939 * @since 1.1.5
940 *
941 * @param string $sdk_prev_version
942 * @param string $sdk_version
943 */
944 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
945 if ( empty( $sdk_prev_version ) ) {
946 return;
947 }
948
949 if (
950 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
951 version_compare( $sdk_version, '2.5.1', '>=' )
952 ) {
953 if ( $this->is_registered( true ) ) {
954 /**
955 * Migrate to new permissions layer.
956 */
957 require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
958
959 $install_by_blog_id = is_multisite() ?
960 $this->get_blog_install_map() :
961 array( 0 => $this->_site );
962
963 fs_migrate_251( $this, $install_by_blog_id );
964 }
965 }
966 }
967
968 /**
969 * @author Leo Fajardo (@leorw)
970 * @since 2.0.0
971 *
972 * @param \FS_Storage $storage
973 * @param bool|int|null $blog_id
974 */
975 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
976 if ( empty( $storage->sdk_version ) ) {
977 // New installation of the plugin, no need to upgrade.
978 return;
979 }
980
981 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
982 // Previous version is >= 2.0.0, so no need to migrate.
983 return;
984 }
985
986 // Alias.
987 $module_type = $storage->get_module_type();
988 $module_slug = $storage->get_module_slug();
989
990 $installs = self::get_all_sites( $module_type, $blog_id );
991 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
992
993 if ( ! is_object( $install ) ) {
994 return;
995 }
996
997 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
998 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
999 $install->plan_id = self::_decrypt( $install->plan->id );
1000 }
1001
1002 unset( $install->plan );
1003
1004 $installs[ $module_slug ] = clone $install;
1005
1006 self::set_account_option_by_module(
1007 $module_type,
1008 'sites',
1009 $installs,
1010 true,
1011 $blog_id
1012 );
1013 }
1014 }
1015
1016 /**
1017 * @author Vova Feldman (@svovaf)
1018 * @since 1.2.2.7
1019 *
1020 * @param string $plugin_prev_version
1021 * @param string $plugin_version
1022 */
1023 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1024 if ( $this->is_theme() ) {
1025 // Expire the cache of the previous tabs since the theme may
1026 // have setting updates.
1027 $this->_cache->expire( 'tabs' );
1028 $this->_cache->expire( 'tabs_stylesheets' );
1029 }
1030 }
1031
1032 /**
1033 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1034 * - Moves some data to the network level storage.
1035 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1036 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1037 * - 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.
1038 * - 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.
1039 * - 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.
1040 *
1041 * @author Vova Feldman (@svovaf)
1042 * @since 2.0.0
1043 */
1044 private static function migrate_accounts_to_network() {
1045 $sites = self::get_sites();
1046 $sites_count = count( $sites );
1047 $connection_status = array();
1048 $plugin_slugs = array();
1049 foreach ( $sites as $site ) {
1050 $blog_id = self::get_site_blog_id( $site );
1051
1052 self::$_accounts->migrate_to_network( $blog_id );
1053
1054 /**
1055 * Build a list of all Freemius powered plugins slugs.
1056 */
1057 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1058 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1059 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1060 $plugin_slugs[ $data['slug'] ] = true;
1061 }
1062 }
1063
1064 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1065
1066 if ( is_array( $installs ) ) {
1067 foreach ( $installs as $slug => $install ) {
1068 if ( ! isset( $connection_status[ $slug ] ) ) {
1069 $connection_status[ $slug ] = array();
1070 }
1071
1072 if ( is_object( $install ) &&
1073 FS_Site::is_valid_id( $install->id ) &&
1074 FS_User::is_valid_id( $install->user_id )
1075 ) {
1076 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1077 }
1078 }
1079 }
1080 }
1081
1082 foreach ( $plugin_slugs as $slug => $true ) {
1083 if ( ! isset( $connection_status[ $slug ] ) ) {
1084 $connection_status[ $slug ] = array();
1085 }
1086
1087 foreach ( $sites as $site ) {
1088 $blog_id = self::get_site_blog_id( $site );
1089
1090 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1091 continue;
1092 }
1093
1094 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1095
1096 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1097
1098 if ( ! is_null( $is_anonymous ) ) {
1099 // Since 1.1.3 is_anonymous is an array.
1100 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1101 $is_anonymous = $is_anonymous['is'];
1102 }
1103
1104 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1105 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1106 }
1107 }
1108
1109 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1110 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1111 }
1112 }
1113 }
1114
1115 $super_admins = array();
1116
1117 foreach ( $connection_status as $slug => $blogs_status ) {
1118 $skips = 0;
1119 $ignores = 0;
1120 $connections = 0;
1121 $opted_in_users = array();
1122 $opted_in_super_admins = array();
1123
1124 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1125
1126 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1127 if ( 'skipped' === $status_or_user_id ) {
1128 $skips ++;
1129 } else if ( 'ignored' === $status_or_user_id ) {
1130 $ignores ++;
1131 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1132 $connections ++;
1133
1134 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1135 $opted_in_users[ $status_or_user_id ] = array();
1136 }
1137
1138 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1139
1140 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1141 self::is_super_admin( $status_or_user_id )
1142 ) {
1143 // Cache super-admin data.
1144 $super_admins[ $status_or_user_id ] = true;
1145
1146 // Remember opted-in super-admins for the plugin.
1147 $opted_in_super_admins[ $status_or_user_id ] = true;
1148 }
1149 }
1150 }
1151
1152 $main_super_admin_user_id = null;
1153 $all_migrated = false;
1154 if ( $sites_count == $skips ) {
1155 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1156 $storage->is_anonymous_ms = $storage->is_anonymous;
1157
1158 $all_migrated = true;
1159 } else if ( $sites_count == $ignores ) {
1160 // Don't do anything, still in activation mode.
1161
1162 $all_migrated = true;
1163 } else if ( 0 < count( $opted_in_super_admins ) ) {
1164 // Find the super-admin with the majority of installs.
1165 $max_installs_by_super_admin = 0;
1166 foreach ( $opted_in_super_admins as $user_id => $true ) {
1167 $installs_count = count( $opted_in_users[ $user_id ] );
1168
1169 if ( $installs_count > $max_installs_by_super_admin ) {
1170 $max_installs_by_super_admin = $installs_count;
1171 $main_super_admin_user_id = $user_id;
1172 }
1173 }
1174
1175 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1176 // Super-admin opted-in for all sites in the network.
1177 $storage->is_network_connected = true;
1178
1179 $all_migrated = true;
1180 }
1181
1182 // Store network user.
1183 $storage->network_user_id = $main_super_admin_user_id;
1184
1185 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1186 // Since all sites are opted-in, associating with the main site.
1187 get_current_blog_id() :
1188 // Associating with the 1st found opted-in site.
1189 $opted_in_users[ $main_super_admin_user_id ][0];
1190
1191 /**
1192 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1193 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1194 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1195 * will be empty.
1196 */
1197 $storage->migrate_to_network();
1198 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1199 } else {
1200 // At least one opt-in. All the opt-in were created by a non-super-admin.
1201 if ( 0 == $ignores ) {
1202 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1203 $storage->store( 'is_delegated_connection', true, true );
1204
1205 $all_migrated = true;
1206 }
1207 }
1208
1209 if ( ! $all_migrated ) {
1210 /**
1211 * Delegate all sites that were:
1212 * 1) Opted-in by a user that is NOT the main-super-admin.
1213 * 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.
1214 */
1215 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1216 if ( $status_or_user_id == $main_super_admin_user_id ) {
1217 continue;
1218 }
1219
1220 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1221 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1222 ) {
1223 $storage->store( 'is_delegated_connection', true, $blog_id );
1224 }
1225 }
1226 }
1227
1228
1229 if ( ( $connections + $skips > 0 ) ) {
1230 if ( $ignores > 0 ) {
1231 /**
1232 * If admin already opted-in or skipped in any of the network sites, and also
1233 * have sites which the connection decision was not yet taken, set this plugin
1234 * into network activation mode so the super-admin can choose what to do with
1235 * the rest of the sites.
1236 */
1237 self::set_network_upgrade_mode( $storage );
1238 }
1239 }
1240 }
1241 }
1242
1243 /**
1244 * Set a module into network upgrade mode.
1245 *
1246 * @author Vova Feldman (@svovaf)
1247 * @since 2.0.0
1248 *
1249 * @param \FS_Storage $storage
1250 *
1251 * @return bool
1252 */
1253 public static function set_network_upgrade_mode( FS_Storage $storage ) {
1254 return $storage->is_network_activation = true;
1255 }
1256
1257 /**
1258 * Will return true after upgrading to the SDK with the network level integration,
1259 * when the super-admin involvement is required regarding the rest of the sites.
1260 *
1261 * @author Vova Feldman (@svovaf)
1262 * @since 2.0.0
1263 *
1264 * @return bool
1265 */
1266 function is_network_upgrade_mode() {
1267 return $this->_storage->get( 'is_network_activation' );
1268 }
1269
1270 /**
1271 * Clear flag after the upgrade mode completion.
1272 *
1273 * @author Vova Feldman (@svovaf)
1274 * @since 2.0.0
1275 *
1276 * @return bool True if network activation was on and now completed.
1277 */
1278 private function network_upgrade_mode_completed() {
1279 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1280 $this->_storage->remove( 'is_network_activation' );
1281
1282 return true;
1283 }
1284
1285 return false;
1286 }
1287
1288 #endregion
1289
1290 /**
1291 * This action is connected to the 'plugins_loaded' hook and helps to determine
1292 * if this is a new plugin installation or a plugin update.
1293 *
1294 * There are 3 different use-cases:
1295 * 1) New plugin installation right with Freemius:
1296 * 1.1 _activate_plugin_event_hook() will be executed first
1297 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1298 * and $this->_storage->plugin_last_version is not set,
1299 * $this->_storage->is_plugin_new_install will be set to TRUE.
1300 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1301 * be already set to TRUE.
1302 *
1303 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1304 * 2.1 _activate_plugin_event_hook() will not be executed, because
1305 * the activation hook do NOT fires on updates since WP 3.1.
1306 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1307 * be empty, therefore, it will be set to FALSE.
1308 *
1309 * 3) Plugin update, had Freemius in prev version as well:
1310 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1311 * before, $this->_storage->plugin_last_version will NOT be empty,
1312 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1313 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1314 * already set, therefore, it will not be modified.
1315 *
1316 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1317 *
1318 * NOTE:
1319 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1320 * and then, the next immediate PageView is the plugin's main settings page, it will not
1321 * show the opt-in right away. The reason it will happen is because Freemius execution
1322 * will be turned off till the plugin is fully loaded at least once
1323 * (till $this->_storage->was_plugin_loaded is TRUE).
1324 *
1325 * @author Vova Feldman (@svovaf)
1326 * @since 1.1.9
1327 *
1328 */
1329 function _plugins_loaded() {
1330 // Update flag that plugin was loaded with Freemius at least once.
1331 $this->_storage->was_plugin_loaded = true;
1332
1333 /**
1334 * Bug fix - only set to false when it's a plugin, due to the
1335 * execution sequence of the theme hooks and our methods, if
1336 * this will be set for themes, Freemius will always assume
1337 * it's a theme update.
1338 *
1339 * @author Vova Feldman (@svovaf)
1340 * @since 1.2.2.2
1341 */
1342 if ( $this->is_plugin() &&
1343 ! isset( $this->_storage->is_plugin_new_install )
1344 ) {
1345 $this->_storage->is_plugin_new_install = (
1346 ! is_plugin_active( $this->_plugin_basename ) &&
1347 empty( $this->_storage->plugin_last_version )
1348 );
1349 }
1350 }
1351
1352 function _run_garbage_collector() {
1353 if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', true ) ) {
1354 return;
1355 }
1356
1357 if ( ! $this->is_user_in_admin() ) {
1358 return;
1359 }
1360
1361 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
1362
1363 $lock = new FS_Lock( 'garbage_collection' );
1364
1365 if ( $lock->is_locked() ) {
1366 return;
1367 }
1368
1369 // Create a 1-day lock.
1370 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
1371
1372 FS_Garbage_Collector::instance()->clean();
1373 }
1374
1375 /**
1376 * Modifies all external links in the submenu by altering their href, and also opens them in new tab if needed.
1377 *
1378 * @author Vova Feldman (@svovaf)
1379 * @author Swashata Ghosh (@swashata)
1380 * @since 2.1.4
1381 */
1382 static function _handle_submenu_external_link() {
1383 ?>
1384 <script type="text/javascript">
1385 (function ( $ ) {
1386 $( '.fs-submenu-item' ).each( function () {
1387 var $this = $( this ),
1388 $parent = $this.parent(),
1389 externalLink = $this.data( 'fs-external-url' ),
1390 isOpensInNewTab = $this.data( 'fs-new-tab' );
1391
1392 if ( externalLink ) {
1393 $parent.attr( 'href', externalLink );
1394 }
1395
1396 if ( isOpensInNewTab ) {
1397 $parent.attr( { target: '_blank', rel: 'noopener noreferrer' } );
1398 }
1399 } );
1400 } )( jQuery );
1401 </script>
1402 <?php
1403 }
1404
1405 /**
1406 * @author Vova Feldman (@svovaf)
1407 * @since 1.0.9
1408 */
1409 private function register_constructor_hooks() {
1410 $this->_logger->entrance();
1411
1412 if ( is_admin() ) {
1413 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1414
1415 if ( $this->is_plugin() ) {
1416 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1417 /**
1418 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1419 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1420 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1421 * updating of a .org plugin).
1422 */
1423 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1424 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1425 /**
1426 * 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.
1427 *
1428 * @author Leo Fajardo (@leorw)
1429 * @since 2.2.3
1430 */
1431 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1432 }
1433
1434 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1435
1436 /**
1437 * @since 1.2.2
1438 *
1439 * Hook to both free and premium version activations to support
1440 * auto deactivation on the other version activation.
1441 */
1442 register_activation_hook(
1443 $plugin_dir . $this->_free_plugin_basename,
1444 array( &$this, '_activate_plugin_event_hook' )
1445 );
1446
1447 register_activation_hook(
1448 $plugin_dir . $this->premium_plugin_basename(),
1449 array( &$this, '_activate_plugin_event_hook' )
1450 );
1451 } else {
1452 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1453
1454 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1455 }
1456
1457 /**
1458 * Part of the mechanism to identify new plugin install vs. plugin update.
1459 *
1460 * @author Vova Feldman (@svovaf)
1461 * @since 1.1.9
1462 */
1463 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1464 /**
1465 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1466 * when the logic gets here since the activation logic first add the activate plugins,
1467 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1468 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1469 * plugin activation, and that IS intentional.
1470 *
1471 * @author Vova Feldman (@svovaf)
1472 */
1473 if ( $this->is_plugin() &&
1474 $this->is_activation_mode( false ) &&
1475 0 == did_action( 'plugins_loaded' )
1476 ) {
1477 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1478 } else {
1479 // If was activated before, then it was already loaded before.
1480 $this->_plugins_loaded();
1481 }
1482 }
1483
1484 add_action( 'plugins_loaded', array( &$this, '_run_garbage_collector' ) );
1485
1486 if ( ! self::is_ajax() ) {
1487 if ( ! $this->is_addon() ) {
1488 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1489 }
1490 }
1491
1492 if ( $this->_storage->handle_gdpr_admin_notice ) {
1493 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1494 }
1495
1496 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1497 add_action( 'init', array( &$this, '_add_pricing_ajax_handler' ) );
1498 }
1499
1500 if ( $this->is_plugin() ) {
1501 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1502 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1503 } else {
1504 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1505 }
1506
1507 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1508 }
1509
1510 if ( is_multisite() ) {
1511 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1512 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1513 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1514
1515 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1516 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1517 } else {
1518 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1519 }
1520
1521 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1522 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1523 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1524 }
1525
1526 if ( $this->is_theme() &&
1527 self::is_customizer() &&
1528 $this->apply_filters( 'show_customizer_upsell', true )
1529 ) {
1530 // Register customizer upsell.
1531 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1532 }
1533
1534 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1535
1536 if ( $this->is_theme() && ! $this->is_migration() ) {
1537 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1538 }
1539
1540 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1541 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1542 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1543 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1544 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1545
1546 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1547 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1548 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1549 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1550
1551 if ( $this->_is_network_active && fs_is_network_admin() ) {
1552 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1553 }
1554
1555 $this->add_ajax_action( 'install_premium_version', array(
1556 &$this,
1557 '_install_premium_version_ajax_action'
1558 ) );
1559
1560 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1561
1562 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1563
1564 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1565
1566 $this->add_action(
1567 'plugin_version_update',
1568 array( &$this, '_after_version_update' ),
1569 WP_FS__DEFAULT_PRIORITY,
1570 2
1571 );
1572 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1573
1574 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) ); // @phpstan-ignore-line
1575 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
1576 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1577
1578 /**
1579 * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
1580 *
1581 * @author Vova Feldman (@svovaf)
1582 * @since 1.2.1.5
1583 */
1584 if (
1585 (
1586 fs_request_is_action( 'reset_anonymous_mode' ) ||
1587 fs_request_is_action( 'reset_pending_activation_mode' )
1588 ) &&
1589 $this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' )
1590 ) {
1591 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1592 }
1593
1594 FS_DebugManager::register_hooks();
1595 }
1596
1597 /**
1598 * Register the required hooks right after the settings parse is completed.
1599 *
1600 * @author Vova Feldman (@svovaf)
1601 * @since 2.3.1
1602 */
1603 private function register_after_settings_parse_hooks() {
1604 if ( is_admin() &&
1605 $this->is_theme() &&
1606 $this->is_premium() &&
1607 ! $this->has_active_valid_license()
1608 ) {
1609 $this->add_ajax_action(
1610 'delete_theme_update_data',
1611 array( &$this, '_delete_theme_update_data_action' )
1612 );
1613 }
1614
1615 if ( $this->show_settings_with_tabs() ) {
1616 /**
1617 * Include the required hooks to capture the theme settings' page tabs
1618 * and cache them.
1619 *
1620 * @author Vova Feldman (@svovaf)
1621 * @since 1.2.2.7
1622 */
1623 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1624 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1625 // Add license activation AJAX callback.
1626 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1627
1628 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1629 }
1630
1631 add_action(
1632 'admin_footer',
1633 array( &$this, '_add_freemius_tabs' ),
1634 /**
1635 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1636 * That's why the priority is 11 while the tabs capture logic is added
1637 * with priority 10.
1638 *
1639 * @author Vova Feldman (@svovaf)
1640 */
1641 11
1642 );
1643 }
1644
1645 if ( ! self::is_ajax() ) {
1646 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1647 add_action(
1648 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1649 array( &$this, '_prepare_admin_menu' ),
1650 WP_FS__LOWEST_PRIORITY
1651 );
1652 }
1653 }
1654 }
1655
1656 /**
1657 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1658 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1659 *
1660 * @author Leo Fajardo (@leorw)
1661 * @since 2.2.3
1662 *
1663 * @param object $updates
1664 * @param string|null $transient
1665 *
1666 * @return object
1667 */
1668 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1669 if ( is_object( $updates ) && isset( $updates->response ) ) {
1670 foreach ( $updates->response as $file => $plugin ) {
1671 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1672 unset( $updates->response[ $file ] );
1673 }
1674 }
1675 }
1676
1677 return $updates;
1678 }
1679
1680 /**
1681 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1682 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1683 *
1684 * @author Leo Fajardo (@leorw)
1685 * @since 2.2.3
1686 *
1687 * @return void
1688 */
1689 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1690 $slug_basename_map = array();
1691 foreach ( self::$_instances as $instance ) {
1692 if ( ! $instance->is_plugin() ) {
1693 continue;
1694 }
1695
1696 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1697 }
1698 ?>
1699 <script type="text/javascript">
1700 (function( $ ) {
1701 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1702 for ( var slug in slugBasenameMap ) {
1703 var basename = slugBasenameMap[ slug ];
1704
1705 // Try to get the plugin rows if on the "Plugins" page.
1706 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1707
1708 if ( 0 === $pluginRows.length ) {
1709 // Try to get the plugin rows if on the "Updates" page.
1710 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1711 if ( 0 !== $pluginCheckbox.length ) {
1712 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1713 }
1714 }
1715
1716 if ( 0 === $pluginRows.length ) {
1717 // No plugin rows found.
1718 continue;
1719 }
1720
1721 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1722 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1723 var $this = $( this ),
1724 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1725
1726 $this.attr( 'href', href );
1727 });
1728 }
1729 })( jQuery );
1730 </script>
1731 <?php
1732 }
1733
1734 /**
1735 * @author Leo Fajardo (@leorw)
1736 * @since 2.3.0
1737 */
1738 static function _maybe_add_beta_label_styles() {
1739 $has_any_beta_version = false;
1740
1741 foreach ( self::$_instances as $instance ) {
1742 if ( $instance->is_beta() ) {
1743 $has_any_beta_version = true;
1744 break;
1745 }
1746 }
1747
1748 if ( $has_any_beta_version ) {
1749 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1750 }
1751 }
1752
1753 /**
1754 * @author Leo Fajardo (@leorw)
1755 * @since 2.3.0
1756 */
1757 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1758 $beta_data = array();
1759
1760 foreach ( self::$_instances as $instance ) {
1761 if ( ! $instance->is_premium() ) {
1762 continue;
1763 }
1764
1765 /**
1766 * If there's an available beta version update, a confirmation message will be shown when the
1767 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1768 */
1769 $has_beta_update = $instance->has_beta_update();
1770
1771 $is_beta = (
1772 // The "Beta" label is added separately for themes.
1773 $instance->is_plugin() &&
1774 $instance->is_beta()
1775 );
1776
1777 if ( ! $is_beta && ! $has_beta_update ) {
1778 continue;
1779 }
1780
1781 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1782
1783 if ( ! $has_beta_update ) {
1784 continue;
1785 }
1786
1787 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1788 '%s %s',
1789 sprintf(
1790 fs_esc_attr_inline(
1791 '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.',
1792 'beta-version-update-caution',
1793 $instance->get_slug()
1794 ),
1795 $instance->get_plugin_title()
1796 ),
1797 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1798 );
1799 }
1800
1801 if ( empty( $beta_data ) ) {
1802 return;
1803 }
1804 ?>
1805 <script type="text/javascript">
1806 ( function( $ ) {
1807 var betaData = <?php echo json_encode( $beta_data ) ?>;
1808
1809 for ( var pluginBasename in betaData ) {
1810 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1811 continue;
1812 }
1813
1814 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1815 continue;
1816 }
1817
1818 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1819 if ( 0 === $parentContainer.length ) {
1820 continue;
1821 }
1822
1823 $parentContainer.find( '.plugin-title > strong:first-child').append(
1824 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1825 );
1826 }
1827
1828 setTimeout( function() {
1829 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1830 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1831 var $parentContainer = $( this ).parents( 'tr:first' );
1832 pluginBasename = ( 0 !== $parentContainer.length ) ?
1833 $parentContainer.data( 'plugin' ) :
1834 $( this ).parents( '.theme:first' ).data( 'slug' );
1835
1836 if (
1837 betaData[ pluginBasename ] &&
1838 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1839 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1840 ) {
1841 return false;
1842 }
1843 } );
1844 }, 20 );
1845 } )( jQuery );
1846 </script>
1847 <?php
1848 }
1849
1850 /**
1851 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1852 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1853 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1854 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1855 *
1856 * @author Leo Fajardo (@leorw)
1857 *
1858 * @since 1.2.0
1859 */
1860 private function unregister_uninstall_hook() {
1861 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1862 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1863 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1864
1865 update_option( 'uninstall_plugins', $uninstallable_plugins );
1866 }
1867
1868 /**
1869 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1870 *
1871 * @param bool $store_prev_path
1872 */
1873 private function clear_module_main_file_cache( $store_prev_path = true ) {
1874 if ( ! isset( $this->_storage->plugin_main_file ) ||
1875 empty( $this->_storage->plugin_main_file->path )
1876 ) {
1877 return;
1878 }
1879
1880 if ( ! $store_prev_path ) {
1881 /**
1882 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1883 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1884 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1885 *
1886 * @author Leo Fajardo (@leorw)
1887 * @since 2.2.1
1888 */
1889 unset( $this->_storage->plugin_main_file->path );
1890 } else {
1891 $plugin_main_file = clone $this->_storage->plugin_main_file;
1892
1893 // Store cached path (2nd layer cache).
1894 $plugin_main_file->prev_path = $plugin_main_file->path;
1895
1896 // Clear cached path.
1897 unset( $plugin_main_file->path );
1898
1899 $this->_storage->plugin_main_file = $plugin_main_file;
1900 }
1901
1902 /**
1903 * Clear global cached path.
1904 *
1905 * @author Leo Fajardo (@leorw)
1906 * @since 1.2.2
1907 */
1908 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1909 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1910 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1911 }
1912
1913 /**
1914 * @author Leo Fajardo (@leorw)
1915 * @since 2.0.0
1916 */
1917 function _hook_action_links_and_register_account_hooks() {
1918 if ( $this->is_migration() ) {
1919 return;
1920 }
1921
1922 if (
1923 ( self::is_plugins_page() && $this->is_plugin() ) ||
1924 ( self::is_themes_page() && $this->is_theme() ) ||
1925 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1926 ) {
1927 $this->_add_tracking_links();
1928 }
1929
1930 if ( self::is_plugins_page() && $this->is_plugin() ) {
1931 $this->hook_plugin_action_links();
1932 }
1933
1934 $this->_register_account_hooks();
1935 }
1936
1937 /**
1938 * @author Vova Feldman (@svovaf)
1939 * @since 1.0.9
1940 */
1941 private function _register_account_hooks() {
1942 if ( ! is_admin() ) {
1943 return;
1944 }
1945
1946 /**
1947 * Always show the deactivation feedback form since we added
1948 * automatic free version deactivation upon premium code activation.
1949 *
1950 * @since 1.2.1.6
1951 */
1952 $this->add_ajax_action(
1953 'submit_uninstall_reason',
1954 array( &$this, '_submit_uninstall_reason_action' )
1955 );
1956
1957 $this->add_ajax_action(
1958 'cancel_subscription_or_trial',
1959 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1960 );
1961
1962 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1963 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1964 ( $this->is_theme() && self::is_themes_page() )
1965 ) {
1966 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1967 }
1968 }
1969 }
1970
1971 /**
1972 * Leverage backtrace to find caller plugin file path.
1973 *
1974 * @param bool $is_init Is initiation sequence.
1975 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
1976 *
1977 * @return string
1978 * @since 1.0.6
1979 *
1980 * @author Vova Feldman (@svovaf)
1981 */
1982 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
1983 // Try to load the cached value of the file path.
1984 if ( isset( $this->_storage->plugin_main_file ) ) {
1985 $plugin_main_file = $this->_storage->plugin_main_file;
1986 if ( ! empty( $plugin_main_file->path ) ) {
1987 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1988 if ( file_exists( $absolute_path ) ) {
1989 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
1990 // Update cached path if not matching the actual path.
1991 $plugin_main_file->path = $main_file;
1992 $this->_storage->plugin_main_file = $plugin_main_file;
1993 }
1994
1995 return $absolute_path;
1996 }
1997 }
1998 }
1999
2000 /**
2001 * @since 1.2.1
2002 *
2003 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
2004 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
2005 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
2006 *
2007 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
2008 * when the class instantiator isn't the module.
2009 */
2010 if ( ! $is_init ) {
2011 // Fetch prev path cache.
2012 if ( isset( $this->_storage->plugin_main_file ) &&
2013 ! empty( $this->_storage->plugin_main_file->prev_path )
2014 ) {
2015 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2016 if ( file_exists( $absolute_path ) ) {
2017 return $absolute_path;
2018 }
2019 }
2020
2021 wp_die(
2022 $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' ) .
2023 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2024 $this->get_text_inline( 'Error', 'error' ),
2025 array( 'back_link' => true )
2026 );
2027 }
2028
2029 /**
2030 * @since 1.2.1
2031 *
2032 * Only the original instantiator that calls dynamic_init can modify the module's path.
2033 */
2034 // Find caller module.
2035 $this->_storage->plugin_main_file = (object) array(
2036 'path' => $main_file,
2037 );
2038
2039 return $this->get_absolute_path( $main_file );
2040 }
2041
2042 /**
2043 * @author Leo Fajardo (@leorw)
2044 * @since 1.2.3
2045 *
2046 * @param string $path
2047 *
2048 * @return string
2049 */
2050 private function get_relative_path( $path ) {
2051 $module_root_dir = $this->get_module_root_dir_path();
2052 if ( 0 === strpos( $path, $module_root_dir ) ) {
2053 $path = substr( $path, strlen( $module_root_dir ) );
2054 }
2055
2056 return $path;
2057 }
2058
2059 /**
2060 * @author Leo Fajardo (@leorw)
2061 * @since 1.2.3
2062 *
2063 * @param string $path
2064 * @param string|bool $module_type
2065 *
2066 * @return string
2067 */
2068 private function get_absolute_path( $path, $module_type = false ) {
2069 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2070 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2071 $path = fs_normalize_path( $module_root_dir . $path );
2072 }
2073
2074 return $path;
2075 }
2076
2077 /**
2078 * @author Leo Fajardo (@leorw)
2079 * @since 1.2.3
2080 *
2081 * @param string|bool $module_type
2082 *
2083 * @return string
2084 */
2085 private function get_module_root_dir_path( $module_type = false ) {
2086 $is_plugin = empty( $module_type ) ?
2087 $this->is_plugin() :
2088 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2089
2090 return fs_normalize_path( trailingslashit( $is_plugin ?
2091 WP_PLUGIN_DIR :
2092 get_theme_root( get_stylesheet() ) ) );
2093 }
2094
2095 /**
2096 * @author Leo Fajardo (@leorw)
2097 *
2098 * @param number $module_id
2099 * @param string $slug
2100 *
2101 * @return string Since 2.5.0 return the module's main file path.
2102 *
2103 * @since 1.2.2
2104 */
2105 private function store_id_slug_type_path_map( $module_id, $slug ) {
2106 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2107
2108 $store_option = false;
2109
2110 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2111 $id_slug_type_path_map[ $module_id ] = array(
2112 'slug' => $slug
2113 );
2114
2115 $store_option = true;
2116 } else if (
2117 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2118 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2119 ) {
2120 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2121 $store_option = true;
2122 }
2123
2124 $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
2125
2126 if ( ! $find_caller ) {
2127 /**
2128 * This verification is for cases when suddenly the same module
2129 * is installed but with a different folder name.
2130 *
2131 * @author Vova Feldman (@svovaf)
2132 * @since 1.2.3
2133 */
2134 $find_caller = ! file_exists( $this->get_absolute_path(
2135 $id_slug_type_path_map[ $module_id ]['path'],
2136 $id_slug_type_path_map[ $module_id ]['type']
2137 ) );
2138 }
2139
2140 foreach ( $id_slug_type_path_map as $id => $data ) {
2141 if ( empty( $id ) ) {
2142 // Remove maps with empty module ID.
2143 unset( $id_slug_type_path_map[ $id ] );
2144 $store_option = true;
2145 continue;
2146 }
2147
2148 /**
2149 * If the module's main file path is identical to the main file path of another module then it means that the cached path of the current module or the other one with the same path is wrong, and therefore, we need to recalculate those paths.
2150 *
2151 * @author Vova Feldman (@svovaf)
2152 * @since 2.5.0
2153 */
2154 if ( ! $find_caller ) {
2155 if ( $id == $module_id ) {
2156 continue;
2157 }
2158
2159 if (
2160 isset( $data['path'] ) &&
2161 $data['path'] === $id_slug_type_path_map[ $module_id ]['path']
2162 ) {
2163 $find_caller = true;
2164 }
2165 }
2166 }
2167
2168 if ( $find_caller ) {
2169 $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
2170
2171 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2172 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2173
2174 $store_option = true;
2175 }
2176
2177 if ( $store_option ) {
2178 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2179 }
2180
2181 return $id_slug_type_path_map[ $module_id ]['path'];
2182 }
2183
2184 /**
2185 * Identifies the caller type: plugin or theme.
2186 *
2187 * @author Leo Fajardo (@leorw)
2188 * @since 1.2.2
2189 *
2190 * @author Vova Feldman (@svovaf)
2191 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2192 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2193 * SDK an internal file instead of directly from functions.php.
2194 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2195 *
2196 * @param number $module_id @since 2.5.0
2197 */
2198 private function get_caller_main_file_and_type( $module_id ) {
2199 self::require_plugin_essentials();
2200
2201 $all_plugins = fs_get_plugins( true );
2202 $all_plugins_paths = array();
2203
2204 // Get active plugin's main files real full names (might be symlinks).
2205 foreach ( $all_plugins as $relative_path => $data ) {
2206 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2207 /**
2208 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2209 * can't really include the SDK.
2210 *
2211 * @author Vova Feldman
2212 * @since 1.2.1.7
2213 */
2214 continue;
2215 }
2216
2217 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2218 }
2219
2220 $caller_file_candidate = false;
2221 $caller_map = array();
2222 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2223 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2224 $plugin_dir_to_skip = false;
2225
2226 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2227 if ( empty( $bt[ $i ]['file'] ) ) {
2228 continue;
2229 }
2230
2231 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2232 // If file same as the prev file in the stack, skip it.
2233 continue;
2234 }
2235
2236 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2237 'do_action',
2238 'apply_filter',
2239 // The string split is stupid, but otherwise, theme check
2240 // throws info notices.
2241 'requir' . 'e_once',
2242 'requir' . 'e',
2243 'includ' . 'e_once',
2244 'includ' . 'e',
2245 'install_and_activate_plugin',
2246 'try_activate_plugin',
2247 'activate_plugin'
2248 ) )
2249 ) {
2250 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2251 /**
2252 * Store the directory of the activator plugin so that any other file that starts with it
2253 * cannot be mistakenly chosen as a candidate caller file.
2254 *
2255 * @author Leo Fajardo
2256 *
2257 * @since 2.3.0
2258 */
2259 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2260
2261 foreach ( $all_plugins_paths as $plugin_path ) {
2262 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2263 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2264 $plugin_dir_to_skip = $plugin_dir;
2265
2266 break;
2267 }
2268 }
2269 }
2270
2271 // Ignore call stack hooks and files inclusion.
2272 continue;
2273 }
2274
2275 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2276
2277 if ( ! empty( $plugin_dir_to_skip ) ) {
2278 /**
2279 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2280 *
2281 * @author Leo Fajardo
2282 *
2283 * @since 2.3.0
2284 */
2285 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2286 continue;
2287 }
2288 }
2289
2290 if ( 'functions.php' === basename( $caller_file_path ) ) {
2291 /**
2292 * 1. Assumes that theme's starting execution file is functions.php.
2293 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2294 *
2295 * @author Vova Feldman (@svovaf)
2296 * @since 1.2.2.5
2297 */
2298
2299 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2300 $module_type = WP_FS__MODULE_TYPE_THEME;
2301
2302 /**
2303 * Relative path of the theme, e.g.:
2304 * `my-theme/functions.php`
2305 *
2306 * @author Leo Fajardo (@leorw)
2307 */
2308 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2309 '/' .
2310 basename( $caller_file_path );
2311
2312 continue;
2313 }
2314 }
2315
2316 $caller_file_hash = md5( $caller_file_path );
2317
2318 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2319 foreach ( $all_plugins_paths as $plugin_path ) {
2320 if ( empty( $plugin_path ) ) {
2321 continue;
2322 }
2323
2324 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2325 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2326 break;
2327 }
2328 }
2329 }
2330
2331 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2332 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2333 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2334 }
2335 }
2336
2337 $caller_main_file_and_type = (object) array(
2338 'module_type' => $module_type,
2339 'path' => $caller_file_candidate
2340 );
2341
2342 return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
2343 }
2344
2345 #----------------------------------------------------------------------------------
2346 #region Deactivation Feedback Form
2347 #----------------------------------------------------------------------------------
2348
2349 /**
2350 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2351 * page.
2352 *
2353 * @author Vova Feldman (@svovaf)
2354 * @author Leo Fajardo (@leorw)
2355 *
2356 * @since 1.1.2
2357 */
2358 function _add_deactivation_feedback_dialog_box() {
2359 if (
2360 $this->is_clone() ||
2361 ( is_object( $this->_site ) && ! $this->is_registered() )
2362 ) {
2363 return;
2364 }
2365
2366 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2367 $this->_get_subscription_cancellation_dialog_box_template_params() :
2368 array();
2369
2370 /**
2371 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2372 */
2373 $show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
2374 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2375 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2376 } else if ( $this->is_addon() ) {
2377 /**
2378 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2379 */
2380 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2381 }
2382
2383 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2384
2385 if (
2386 empty( $subscription_cancellation_dialog_box_template_params ) &&
2387 ! $show_deactivation_feedback_form &&
2388 empty( $uninstall_confirmation_message )
2389 ) {
2390 return;
2391 }
2392
2393 $vars = array( 'id' => $this->_module_id );
2394
2395 if ( $show_deactivation_feedback_form ) {
2396 /* Check the type of user:
2397 * 1. Long-term (long-term)
2398 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2399 * 3. Short-term (short-term)
2400 */
2401 $is_long_term_user = true;
2402
2403 // Check if the site is at least 2 days old.
2404 $time_installed = $this->_storage->install_timestamp;
2405
2406 // Difference in seconds.
2407 $date_diff = time() - $time_installed;
2408
2409 // Convert seconds to days.
2410 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2411
2412 if ( $date_diff_days < 2 ) {
2413 $is_long_term_user = false;
2414 }
2415
2416 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2417
2418 if ( $is_long_term_user ) {
2419 $user_type = 'long-term';
2420 } else {
2421 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2422 $user_type = 'non-registered-and-non-anonymous-short-term';
2423 } else {
2424 $user_type = 'short-term';
2425 }
2426 }
2427
2428 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2429
2430 $vars['reasons'] = $uninstall_reasons;
2431 }
2432
2433 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2434 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2435 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2436
2437 /**
2438 * Load the HTML template for the deactivation feedback dialog box.
2439 *
2440 * @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.
2441 */
2442 fs_require_template( 'forms/deactivation/form.php', $vars );
2443 }
2444
2445 /**
2446 * @author Leo Fajardo (@leorw)
2447 * @since 1.1.2
2448 *
2449 * @param string $user_type
2450 *
2451 * @return array The uninstall reasons for the specified user type.
2452 */
2453 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2454 $module_type = $this->_module_type;
2455
2456 $internal_message_template_var = array(
2457 'id' => $this->_module_id
2458 );
2459
2460 $plan = $this->get_plan();
2461
2462 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2463 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2464 } else {
2465 $contact_support_template = '';
2466 }
2467
2468 $reason_found_better_plugin = array(
2469 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2470 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2471 'input_type' => 'textfield',
2472 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2473 );
2474
2475 $reason_temporary_deactivation = array(
2476 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2477 'text' => sprintf(
2478 $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
2479 strtolower( $this->is_plugin() ?
2480 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2481 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2482 )
2483 ),
2484 'input_type' => '',
2485 'input_placeholder' => ''
2486 );
2487
2488 $reason_other = array(
2489 'id' => self::REASON_OTHER,
2490 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2491 'input_type' => 'textfield',
2492 'input_placeholder' => ''
2493 );
2494
2495 $long_term_user_reasons = array(
2496 array(
2497 'id' => self::REASON_NO_LONGER_NEEDED,
2498 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2499 'input_type' => '',
2500 'input_placeholder' => ''
2501 ),
2502 $reason_found_better_plugin,
2503 array(
2504 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2505 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2506 'input_type' => '',
2507 'input_placeholder' => ''
2508 ),
2509 array(
2510 'id' => self::REASON_BROKE_MY_SITE,
2511 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2512 'input_type' => '',
2513 'input_placeholder' => '',
2514 'internal_message' => $contact_support_template
2515 ),
2516 array(
2517 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2518 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2519 'input_type' => '',
2520 'input_placeholder' => '',
2521 'internal_message' => $contact_support_template
2522 )
2523 );
2524
2525 if ( $this->is_paying() ) {
2526 $long_term_user_reasons[] = array(
2527 'id' => self::REASON_CANT_PAY_ANYMORE,
2528 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2529 'input_type' => 'textfield',
2530 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2531 );
2532 }
2533
2534 $reason_dont_share_info = array(
2535 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2536 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2537 'input_type' => '',
2538 'input_placeholder' => ''
2539 );
2540
2541 /**
2542 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2543 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2544 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2545 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2546 * button in the opt-in form is shown/hidden).
2547 */
2548 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2549 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2550 }
2551
2552 $uninstall_reasons = array(
2553 'long-term' => $long_term_user_reasons,
2554 'non-registered-and-non-anonymous-short-term' => array(
2555 array(
2556 'id' => self::REASON_DIDNT_WORK,
2557 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2558 'input_type' => '',
2559 'input_placeholder' => ''
2560 ),
2561 $reason_dont_share_info,
2562 $reason_found_better_plugin
2563 ),
2564 'short-term' => array(
2565 array(
2566 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2567 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2568 'input_type' => '',
2569 'input_placeholder' => '',
2570 'internal_message' => $contact_support_template
2571 ),
2572 $reason_found_better_plugin,
2573 array(
2574 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2575 '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 ),
2576 'input_type' => 'textarea',
2577 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2578 ),
2579 array(
2580 'id' => self::REASON_NOT_WORKING,
2581 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2582 'input_type' => 'textarea',
2583 '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' )
2584 ),
2585 array(
2586 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2587 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2588 'input_type' => 'textarea',
2589 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2590 ),
2591 array(
2592 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2593 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2594 'input_type' => 'textarea',
2595 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2596 )
2597 )
2598 );
2599
2600 // Randomize the reasons for the current user type.
2601 shuffle( $uninstall_reasons[ $user_type ] );
2602
2603 // Keep the following reasons as the last items in the list.
2604 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2605 $uninstall_reasons[ $user_type ][] = $reason_other;
2606
2607 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2608
2609 return $uninstall_reasons[ $user_type ];
2610 }
2611
2612 /**
2613 * Called after the user has submitted his reason for deactivating the plugin.
2614 *
2615 * @author Leo Fajardo (@leorw)
2616 * @since 1.1.2
2617 */
2618 function _submit_uninstall_reason_action() {
2619 $this->_logger->entrance();
2620
2621 $this->check_ajax_referer( 'submit_uninstall_reason' );
2622
2623 $reason_id = fs_request_get( 'reason_id' );
2624
2625 // Check if the given reason ID is an unsigned integer.
2626 if ( ! ctype_digit( $reason_id ) ) {
2627 exit;
2628 }
2629
2630 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2631 if ( ! empty( $reason_info ) ) {
2632 $reason_info = substr( $reason_info, 0, 128 );
2633 }
2634
2635 $reason = (object) array(
2636 'id' => $reason_id,
2637 'info' => $reason_info,
2638 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2639 );
2640
2641 $this->_storage->store( 'uninstall_reason', $reason );
2642
2643 if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) {
2644 $snooze_period = fs_request_get( 'snooze_period' );
2645
2646 if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) {
2647 self::snooze_deactivation_form( (int) $snooze_period );
2648 }
2649 }
2650
2651 /**
2652 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2653 * not support uninstall hook.
2654 *
2655 * @author Leo Fajardo (@leorw)
2656 * @since 1.2.2
2657 */
2658 if ( $this->is_theme() ) {
2659 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2660 FS_Plugin_Updater::instance( $this )->delete_update_data();
2661 }
2662
2663 $this->_uninstall_plugin_event( false );
2664 $this->remove_sdk_reference();
2665 }
2666
2667 // Print '1' for successful operation.
2668 echo 1;
2669 exit;
2670 }
2671
2672 #--------------------------------------------------------------------------------
2673 #region Deactivation Feedback Snoozing
2674 #--------------------------------------------------------------------------------
2675
2676 /**
2677 * @author Vova Feldman (@svovaf)
2678 * @since 2.4.3
2679 *
2680 * @param int $period
2681 *
2682 * @return bool True if the value was set, false otherwise.
2683 */
2684 private static function snooze_deactivation_form( $period ) {
2685 return ( 0 < $period && self::reset_deactivation_snoozing( $period ) );
2686 }
2687
2688 /**
2689 * Check if deactivation feedback form is snoozed.
2690 *
2691 * @author Vova Feldman (@svovaf)
2692 * @since 2.4.3
2693 *
2694 * @return bool
2695 */
2696 static function is_deactivation_snoozed() {
2697 $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ?
2698 get_transient( 'fs_snooze_period' ) :
2699 get_site_transient( 'fs_snooze_period' );
2700
2701
2702 return ( 'true' === $is_snoozed );
2703 }
2704
2705 /**
2706 * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period.
2707 *
2708 * @param int $period Period in seconds.
2709 *
2710 * @author Vova Feldman (@svovaf)
2711 * @since 2.4.3
2712 */
2713 public static function reset_deactivation_snoozing( $period = 0 ) {
2714 $value = ( 0 === $period ) ? null : 'true';
2715
2716 if ( ! is_multisite() || fs_is_network_admin() ) {
2717 return set_transient( 'fs_snooze_period', $value, $period );
2718 } else {
2719 return set_site_transient( 'fs_snooze_period', $value, $period );
2720 }
2721 }
2722
2723 /**
2724 * The deactivation snooze expiration UNIX timestamp (in sec).
2725 *
2726 * @author Vova Feldman (@svovaf)
2727 * @since 2.4.3
2728 *
2729 * @return int
2730 */
2731 static function deactivation_snooze_expires_at() {
2732 return ( ! is_multisite() || fs_is_network_admin() ) ?
2733 (int) get_option( '_transient_timeout_fs_snooze_period' ) :
2734 (int) get_site_option( '_site_transient_timeout_fs_snooze_period' );
2735 }
2736
2737 #endregion
2738
2739 /**
2740 * @author Leo Fajardo (@leorw)
2741 * @since 2.1.4
2742 */
2743 function cancel_subscription_or_trial_ajax_action() {
2744 $this->_logger->entrance();
2745
2746 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2747
2748 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2749
2750 if ( $this->is_api_error( $result ) ) {
2751 $this->shoot_ajax_failure( $result->error->message );
2752 }
2753
2754 $this->shoot_ajax_success();
2755 }
2756
2757 /**
2758 * @author Leo Fajardo (@leorw)
2759 * @since 2.1.4
2760 *
2761 * @param number $plugin_id
2762 *
2763 * @return object
2764 */
2765 private function cancel_subscription_or_trial( $plugin_id ) {
2766 $fs = null;
2767 if ( $plugin_id == $this->get_id() ) {
2768 $fs = $this;
2769 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2770 $fs = self::get_instance_by_id( $plugin_id );
2771 }
2772
2773 $result = null;
2774
2775 if ( ! is_null( $fs ) ) {
2776 $result = $fs->is_paid_trial() ?
2777 $fs->_cancel_trial() :
2778 $fs->_downgrade_site();
2779 }
2780
2781 return $result;
2782 }
2783
2784 /**
2785 * @author Leo Fajardo (@leorw)
2786 * @since 2.0.2
2787 */
2788 function _delete_theme_update_data_action() {
2789 FS_Plugin_Updater::instance( $this )->delete_update_data();
2790 }
2791
2792 #endregion
2793
2794 #----------------------------------------------------------------------------------
2795 #region Instance
2796 #----------------------------------------------------------------------------------
2797
2798 /**
2799 * Main singleton instance.
2800 *
2801 * @author Vova Feldman (@svovaf)
2802 * @since 1.0.0
2803 *
2804 * @param number $module_id
2805 * @param string|bool $slug
2806 * @param bool $is_init Is initiation sequence.
2807 *
2808 * @return Freemius|false
2809 */
2810 static function instance( $module_id, $slug = false, $is_init = false ) {
2811 if ( empty( $module_id ) ) {
2812 return false;
2813 }
2814
2815 /**
2816 * 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.
2817 */
2818 self::_load_required_static();
2819
2820 if ( ! is_numeric( $module_id ) ) {
2821 if ( ! $is_init && true === $slug ) {
2822 $is_init = true;
2823 }
2824
2825 $slug = $module_id;
2826
2827 $module = FS_Plugin_Manager::instance( $slug )->get();
2828
2829 if ( is_object( $module ) ) {
2830 $module_id = $module->id;
2831 }
2832 }
2833
2834 $key = 'm_' . $module_id;
2835
2836 if ( ! isset( self::$_instances[ $key ] ) ) {
2837 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2838 }
2839
2840 return self::$_instances[ $key ];
2841 }
2842
2843 /**
2844 * @author Vova Feldman (@svovaf)
2845 * @since 1.0.6
2846 *
2847 * @param number $addon_id
2848 *
2849 * @return bool
2850 */
2851 private static function has_instance( $addon_id ) {
2852 return isset( self::$_instances[ 'm_' . $addon_id ] );
2853 }
2854
2855 /**
2856 * @author Leo Fajardo (@leorw)
2857 * @since 1.2.2
2858 *
2859 * @param string|number $id_or_slug
2860 * @param string $module_type
2861 *
2862 * @return number|false
2863 */
2864 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2865 if ( is_numeric( $id_or_slug ) ) {
2866 return $id_or_slug;
2867 }
2868
2869 foreach ( self::$_instances as $instance ) {
2870 // Also check the module type since there can be a plugin and a theme with the same slug.
2871 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2872 return $instance->get_id();
2873 }
2874 }
2875
2876 return false;
2877 }
2878
2879 /**
2880 * @author Vova Feldman (@svovaf)
2881 * @since 1.0.6
2882 *
2883 * @param number $id
2884 *
2885 * @return false|Freemius
2886 */
2887 static function get_instance_by_id( $id ) {
2888 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2889 self::$_instances[ 'm_' . $id ] :
2890 false;
2891 }
2892
2893 /**
2894 *
2895 * @author Vova Feldman (@svovaf)
2896 * @since 1.0.1
2897 *
2898 * @param string $plugin_file
2899 * @param string $module_type
2900 *
2901 * @return false|Freemius
2902 */
2903 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2904 $slug = self::find_slug_by_basename( $plugin_file );
2905
2906 return ( false !== $slug ) ?
2907 self::instance( self::get_module_id( $slug, $module_type ) ) :
2908 false;
2909 }
2910
2911 /**
2912 * @author Vova Feldman (@svovaf)
2913 * @since 1.0.6
2914 *
2915 * @return false|Freemius
2916 */
2917 function get_parent_instance() {
2918 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2919 }
2920
2921 /**
2922 * @author Vova Feldman (@svovaf)
2923 * @since 1.0.6
2924 *
2925 * @param string|number $id_or_slug
2926 *
2927 * @return false|Freemius
2928 */
2929 function get_addon_instance( $id_or_slug ) {
2930 $addon_id = self::get_module_id( $id_or_slug );
2931
2932 return self::instance( $addon_id );
2933 }
2934
2935 /**
2936 * @return Freemius[]
2937 */
2938 static function _get_all_instances() {
2939 return self::$_instances;
2940 }
2941
2942 #endregion ------------------------------------------------------------------
2943
2944 /**
2945 * @author Vova Feldman (@svovaf)
2946 * @since 1.0.6
2947 *
2948 * @return bool
2949 */
2950 function is_parent_plugin_installed() {
2951 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2952
2953 if ( $is_active ) {
2954 return true;
2955 }
2956
2957 /**
2958 * Parent module might be a theme. If that's the case, the add-on's FS
2959 * instance will be loaded prior to the theme's FS instance, therefore,
2960 * we need to check if it's active with a "look ahead".
2961 *
2962 * @author Vova Feldman
2963 * @since 1.2.2.3
2964 */
2965 global $fs_active_plugins;
2966 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2967 $active_theme = wp_get_theme();
2968
2969 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2970 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2971 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2972 // Parent module is a theme and it's currently active.
2973 return true;
2974 }
2975 }
2976 }
2977 }
2978
2979 return false;
2980 }
2981
2982 /**
2983 * Check if add-on parent plugin in activation mode.
2984 *
2985 * @author Vova Feldman (@svovaf)
2986 * @since 1.0.7
2987 *
2988 * @return bool
2989 */
2990 function is_parent_in_activation() {
2991 $parent_fs = $this->get_parent_instance();
2992 if ( ! is_object( $parent_fs ) ) {
2993 return false;
2994 }
2995
2996 return ( $parent_fs->is_activation_mode() );
2997 }
2998
2999 /**
3000 * Is plugin in activation mode.
3001 *
3002 * @author Vova Feldman (@svovaf)
3003 * @since 1.0.7
3004 *
3005 * @param bool $and_on
3006 *
3007 * @return bool
3008 */
3009 function is_activation_mode( $and_on = true ) {
3010 return fs_is_network_admin() ?
3011 $this->is_network_activation_mode( $and_on ) :
3012 $this->is_site_activation_mode( $and_on );
3013 }
3014
3015 /**
3016 * Is plugin in activation mode.
3017 *
3018 * @author Vova Feldman (@svovaf)
3019 * @since 1.0.7
3020 *
3021 * @param bool $and_on
3022 *
3023 * @return bool
3024 */
3025 function is_site_activation_mode( $and_on = true ) {
3026 return (
3027 ( $this->is_on() || ! $and_on ) &&
3028 (
3029 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
3030 (
3031 ( ! $this->is_registered() ||
3032 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
3033 ( ! $this->is_enable_anonymous() ||
3034 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
3035 )
3036 )
3037 );
3038 }
3039
3040 /**
3041 * Checks if the SDK in network activation mode.
3042 *
3043 * @author Leo Fajardo (@leorw)
3044 * @since 2.0.0
3045 *
3046 * @param bool $and_on
3047 *
3048 * @return bool
3049 */
3050 private function is_network_activation_mode( $and_on = true ) {
3051 if ( ! $this->_is_network_active ) {
3052 // Not network activated.
3053 return false;
3054 }
3055
3056 if ( $this->is_network_upgrade_mode() ) {
3057 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3058 return true;
3059 }
3060
3061 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3062 // 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.
3063 return false;
3064 }
3065
3066 if ( $this->is_network_delegated_connection() ) {
3067 // Super-admin delegated the connection to the site admins -> not activation mode.
3068 return false;
3069 }
3070
3071 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3072 // Super-admin skipped the connection network wide -> not activation mode.
3073 return false;
3074 }
3075
3076 if ( $this->is_network_registered() ) {
3077 // Super-admin connected at least one site -> not activation mode.
3078 return false;
3079 }
3080
3081 return true;
3082 }
3083
3084 /**
3085 * Check if current page is the opt-in/pending-activation page.
3086 *
3087 * @author Vova Feldman (@svovaf)
3088 * @since 1.2.1.7
3089 *
3090 * @return bool
3091 */
3092 function is_activation_page() {
3093 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3094 return true;
3095 }
3096
3097 if ( ! $this->is_activation_mode() ) {
3098 return false;
3099 }
3100
3101 // Check if current page is matching the activation page.
3102 return $this->is_matching_url( $this->get_activation_url() );
3103 }
3104
3105 /**
3106 * Check if URL path's are matching and that all querystring
3107 * arguments of the $sub_url exist in the $url with the same values.
3108 *
3109 * WARNING:
3110 * 1. This method doesn't check if the sub/domain are matching.
3111 * 2. Ignore case sensitivity.
3112 *
3113 * @author Vova Feldman (@svovaf)
3114 * @since 1.2.1.7
3115 *
3116 * @param string $sub_url
3117 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3118 *
3119 * @return bool
3120 */
3121 private function is_matching_url( $sub_url, $url = '' ) {
3122 if ( empty( $url ) ) {
3123 $url = $_SERVER['REQUEST_URI'];
3124 }
3125
3126 $url = strtolower( $url );
3127 $sub_url = strtolower( $sub_url );
3128
3129 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3130 // Different path - DO NOT OVERRIDE PAGE.
3131 return false;
3132 }
3133
3134 $url_params = fs_parse_url_params( $url );
3135 $sub_url_params = fs_parse_url_params( $sub_url );
3136
3137 foreach ( $sub_url_params as $key => $val ) {
3138 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3139 // Not matching query string - DO NOT OVERRIDE PAGE.
3140 return false;
3141 }
3142 }
3143
3144 return true;
3145 }
3146
3147 /**
3148 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3149 *
3150 * @author Vova Feldman (@svovaf)
3151 * @since 2.0.0
3152 *
3153 * @param int $blog_id
3154 *
3155 * @return string[]
3156 */
3157 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3158 if ( is_multisite() && $blog_id > 0 ) {
3159 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3160 } else {
3161 $active_basenames = get_option( 'active_plugins' );
3162 }
3163
3164 if ( ! is_array( $active_basenames ) ) {
3165 $active_basenames = array();
3166 }
3167
3168 if ( is_multisite() ) {
3169 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3170
3171 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3172 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3173 }
3174 }
3175
3176 return $active_basenames;
3177 }
3178
3179 /**
3180 * @author Leo Fajardo (@leorw)
3181 * @since 2.3.0
3182 *
3183 * @param int $blog_id
3184 *
3185 * @return array
3186 */
3187 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3188 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3189
3190 $map = array();
3191
3192 foreach ( $active_basenames as $active_basename ) {
3193 $active_basename = fs_normalize_path( $active_basename );
3194
3195 if ( false === strpos( $active_basename, '/' ) ) {
3196 continue;
3197 }
3198
3199 $map[ dirname( $active_basename ) ] = true;
3200 }
3201
3202 return $map;
3203 }
3204
3205 /**
3206 * Get collection of all active plugins. Including network activated plugins.
3207 *
3208 * @author Vova Feldman (@svovaf)
3209 * @since 1.0.9
3210 *
3211 * @param int $blog_id Since 2.0.0
3212 *
3213 * @return array[string]array
3214 */
3215 private static function get_active_plugins( $blog_id = 0 ) {
3216 self::require_plugin_essentials();
3217
3218 $active_plugin = array();
3219 $all_plugins = fs_get_plugins();
3220 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3221
3222 foreach ( $active_plugins_basenames as $plugin_basename ) {
3223 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3224 }
3225
3226 return $active_plugin;
3227 }
3228
3229 /**
3230 * Get collection of all site active plugins for a specified blog.
3231 *
3232 * @author Vova Feldman (@svovaf)
3233 * @since 2.0.0
3234 *
3235 * @param int $blog_id
3236 *
3237 * @return array[string]array
3238 */
3239 private static function get_site_active_plugins( $blog_id = 0 ) {
3240 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3241 get_blog_option( $blog_id, 'active_plugins' ) :
3242 get_option( 'active_plugins' );
3243
3244 $active = array();
3245
3246 if ( ! is_array( $active_basenames ) ) {
3247 return $active;
3248 }
3249
3250 foreach ( $active_basenames as $basename ) {
3251 $active[ $basename ] = array(
3252 'is_active' => true,
3253 'Version' => '1.0', // Dummy version.
3254 'slug' => self::get_plugin_slug( $basename ),
3255 );
3256 }
3257
3258 return $active;
3259 }
3260
3261 /**
3262 * Get collection of all plugins with their activation status for a specified blog.
3263 *
3264 * @author Vova Feldman (@svovaf)
3265 * @since 1.1.8
3266 *
3267 * @param int $blog_id Since 2.0.0
3268 *
3269 * @return array Key is the plugin file path and the value is an array of the plugin data.
3270 */
3271 private static function get_all_plugins( $blog_id = 0 ) {
3272 self::require_plugin_essentials();
3273
3274 $all_plugins = fs_get_plugins();
3275
3276 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3277
3278 foreach ( $all_plugins as $basename => &$data ) {
3279 // By default set to inactive (next foreach update the active plugins).
3280 $data['is_active'] = false;
3281 // Enrich with plugin slug.
3282 $data['slug'] = self::get_plugin_slug( $basename );
3283 }
3284
3285 // Flag active plugins.
3286 foreach ( $active_plugins_basenames as $basename ) {
3287 if ( isset( $all_plugins[ $basename ] ) ) {
3288 $all_plugins[ $basename ]['is_active'] = true;
3289 }
3290 }
3291
3292 return $all_plugins;
3293 }
3294
3295 /**
3296 * Get collection of all plugins and if they are network level activated.
3297 *
3298 * @author Vova Feldman (@svovaf)
3299 * @since 2.0.0
3300 *
3301 * @return array Key is the plugin basename and the value is an array of the plugin data.
3302 */
3303 private static function get_network_plugins() {
3304 self::require_plugin_essentials();
3305
3306 $all_plugins = fs_get_plugins();
3307
3308 $network_active_basenames = is_multisite() ?
3309 get_site_option( 'active_sitewide_plugins' ) :
3310 array();
3311
3312 foreach ( $all_plugins as $basename => &$data ) {
3313 // By default set to inactive (next foreach update the active plugins).
3314 $data['is_active'] = false;
3315 // Enrich with plugin slug.
3316 $data['slug'] = self::get_plugin_slug( $basename );
3317 }
3318
3319 // Flag active plugins.
3320 foreach ( $network_active_basenames as $basename ) {
3321 if ( isset( $all_plugins[ $basename ] ) ) {
3322 $all_plugins[ $basename ]['is_active'] = true;
3323 }
3324 }
3325
3326 return $all_plugins;
3327 }
3328
3329 /**
3330 * Cached result of get_site_transient( 'update_plugins' )
3331 *
3332 * @author Vova Feldman (@svovaf)
3333 * @since 1.1.8
3334 *
3335 * @var object
3336 */
3337 private static $_plugins_info;
3338
3339 /**
3340 * Helper function to get specified plugin's slug.
3341 *
3342 * @author Vova Feldman (@svovaf)
3343 * @since 1.1.8
3344 *
3345 * @param $basename
3346 *
3347 * @return string
3348 */
3349 private static function get_plugin_slug( $basename ) {
3350 if ( ! isset( self::$_plugins_info ) ) {
3351 self::$_plugins_info = get_site_transient( 'update_plugins' );
3352 }
3353
3354 $slug = '';
3355
3356 if ( is_object( self::$_plugins_info ) ) {
3357 if ( isset( self::$_plugins_info->no_update ) &&
3358 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3359 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3360 ) {
3361 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3362 } else if ( isset( self::$_plugins_info->response ) &&
3363 isset( self::$_plugins_info->response[ $basename ] ) &&
3364 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3365 ) {
3366 $slug = self::$_plugins_info->response[ $basename ]->slug;
3367 }
3368 }
3369
3370 if ( empty( $slug ) ) {
3371 // Try to find slug from FS data.
3372 $slug = self::find_slug_by_basename( $basename );
3373 }
3374
3375 if ( empty( $slug ) ) {
3376 // Fallback to plugin's folder name.
3377 $slug = dirname( $basename );
3378 }
3379
3380 return $slug;
3381 }
3382
3383 private static $_statics_loaded = false;
3384
3385 /**
3386 * Load static resources.
3387 *
3388 * @author Vova Feldman (@svovaf)
3389 * @since 1.0.1
3390 */
3391 private static function _load_required_static() {
3392 if ( self::$_statics_loaded ) {
3393 return;
3394 }
3395
3396 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3397
3398 self::$_static_logger->entrance();
3399
3400 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3401
3402 if ( is_multisite() ) {
3403 $has_skipped_migration = (
3404 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3405 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3406 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3407 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3408 );
3409
3410 /**
3411 * If the file_slug_map exists on the site level but doesn't exist on the
3412 * network level storage, it means that we need to process the storage with migration.
3413 *
3414 * 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.
3415 *
3416 * @author Vova Feldman (@svovaf)
3417 * @since 2.0.0
3418 */
3419 if (
3420 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3421 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3422 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3423 ) {
3424 self::migrate_options_to_network();
3425 }
3426 }
3427
3428 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3429
3430 FS_DebugManager::load_required_static();
3431
3432 if ( 0 == did_action( 'init' ) ) {
3433 add_action( 'init', array( 'Freemius', '_load_textdomain' ), 1 );
3434 }
3435
3436 $clone_manager = FS_Clone_Manager::instance();
3437 add_action( 'init', array( $clone_manager, '_init' ) );
3438
3439 add_action( 'admin_footer', array( 'Freemius', '_handle_submenu_external_link' ) );
3440
3441 if ( self::is_plugins_page() || self::is_themes_page() ) {
3442 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3443
3444 /**
3445 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3446 * page.
3447 *
3448 * @author Leo Fajardo (@leorw)
3449 * @since 2.3.0
3450 */
3451 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3452 }
3453
3454 self::$_statics_loaded = true;
3455 }
3456
3457 public static function get_static_logger() {
3458 return self::$_static_logger;
3459 }
3460
3461 public static function get_accounts() {
3462 return self::$_accounts;
3463 }
3464
3465 #--------------------------------------------------------------------------------
3466 #region Clone
3467 #--------------------------------------------------------------------------------
3468
3469 /**
3470 * @author Leo Fajardo (@leorw)
3471 * @since 2.5.0
3472 *
3473 * @param bool $only_if_manual_resolution_is_not_hidden
3474 *
3475 * @return bool
3476 */
3477 private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3478 if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) {
3479 return false;
3480 }
3481
3482 return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired();
3483 }
3484
3485 /**
3486 * @author Leo Fajardo (@leorw)
3487 * @since 2.5.0
3488 *
3489 * @param bool $only_if_manual_resolution_is_not_hidden
3490 */
3491 function is_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3492 if ( ! is_object( $this->_site ) ) {
3493 return false;
3494 }
3495
3496 $blog_id = null;
3497
3498 if (
3499 fs_is_network_admin() &&
3500 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
3501 ) {
3502 // Ensure that we're comparing the network install's URL with the relevant subsite's URL.
3503 $blog_id = $this->_storage->network_install_blog_id;
3504 }
3505
3506 $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
3507
3508 if ( ! $this->_site->is_clone( $site_url ) ) {
3509 return false;
3510 }
3511
3512 return (
3513 ! $only_if_manual_resolution_is_not_hidden ||
3514 ! FS_Clone_Manager::instance()->should_hide_manual_resolution()
3515 );
3516 }
3517
3518 /**
3519 * @author Leo Fajardo (@leorw)
3520 * @since 2.5.0
3521 *
3522 * @param int|null $blog_id
3523 * @param bool $strip_protocol
3524 * @param bool $add_trailing_slash
3525 *
3526 * @return string
3527 */
3528 static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) {
3529 $url = ( ! is_multisite() && defined( 'WP_SITEURL' ) ) ? WP_SITEURL : self::get_site_url_from_wp_option( $blog_id );
3530
3531 if ( $strip_protocol ) {
3532 $url = fs_strip_url_protocol( $url );
3533 }
3534
3535 if ( $add_trailing_slash ) {
3536 $url = trailingslashit( $url );
3537 }
3538
3539 return $url;
3540 }
3541
3542 /**
3543 * @author Leo Fajardo (@leorw)
3544 * @since 2.6.0
3545 *
3546 * @param int|null $blog_id
3547 *
3548 * @return string
3549 */
3550 private static function get_site_url_from_wp_option( $blog_id = null ) {
3551 global $wp_filter;
3552
3553 $site_url_filters = array(
3554 'site_url' => null,
3555 'pre_option_siteurl' => null,
3556 'default_option_siteurl' => null,
3557 'option_siteurl' => null,
3558 );
3559
3560 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3561 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3562 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3563 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3564 unset( $wp_filter[ $hook_name ] );
3565 }
3566 }
3567
3568 $url = get_site_url( $blog_id );
3569
3570 // Re-attach the filters back.
3571 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3572 if ( ! empty( $site_url_filter ) ) {
3573 $wp_filter[ $hook_name ] = $site_url_filter;
3574 }
3575 }
3576
3577 return $url;
3578 }
3579
3580 /**
3581 * @author Leo Fajardo (@leorw)
3582 * @since 2.5.0
3583 *
3584 * @param number $site_id
3585 */
3586 function fetch_install_by_id( $site_id ) {
3587 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3588 }
3589
3590 /**
3591 * @author Leo Fajardo (@leorw)
3592 * @since 2.5.0
3593 *
3594 * @return string|object|bool
3595 */
3596 function _handle_long_term_duplicate() {
3597 $this->_logger->entrance();
3598
3599 $this->delete_current_install( false );
3600
3601 $license_key = false;
3602
3603 if (
3604 is_object( $this->_license ) &&
3605 ! $this->_license->is_utilized(
3606 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3607 )
3608 ) {
3609 $license_key = $this->_license->secret_key;
3610 }
3611
3612 return $this->opt_in(
3613 false,
3614 false,
3615 false,
3616 $license_key,
3617 false,
3618 false,
3619 false,
3620 null,
3621 array(),
3622 false
3623 );
3624 }
3625
3626 #endregion
3627
3628 /**
3629 * @author Leo Fajardo (@leorw)
3630 *
3631 * @since 2.1.3
3632 */
3633 public static function migrate_options_to_network() {
3634 self::migrate_accounts_to_network();
3635
3636 // Migrate API options from site level to network level.
3637 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3638 $api_network_options->migrate_to_network();
3639
3640 // Migrate API cache to network level storage.
3641 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3642
3643 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3644 }
3645
3646 #----------------------------------------------------------------------------------
3647 #region Localization
3648 #----------------------------------------------------------------------------------
3649
3650 /**
3651 * Load framework's text domain.
3652 *
3653 * @author Vova Feldman (@svovaf)
3654 * @since 1.2.1
3655 */
3656 static function _load_textdomain() {
3657 if ( ! is_admin() ) {
3658 return;
3659 }
3660
3661 global $fs_active_plugins;
3662
3663 // Works both for plugins and themes.
3664 load_plugin_textdomain(
3665 'freemius',
3666 false,
3667 $fs_active_plugins->newest->sdk_path . '/languages/'
3668 );
3669 }
3670
3671 #endregion
3672
3673 #----------------------------------------------------------------------------------
3674 #region Connectivity Issues
3675 #----------------------------------------------------------------------------------
3676
3677 /**
3678 * Check if Freemius should be turned on for the current plugin install.
3679 *
3680 * Note:
3681 * $this->_is_on is updated in has_api_connectivity()
3682 *
3683 * @author Vova Feldman (@svovaf)
3684 * @since 1.0.9
3685 *
3686 * @return bool
3687 */
3688 function is_on() {
3689 self::$_static_logger->entrance();
3690
3691 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
3692 return false;
3693 }
3694
3695 if ( isset( $this->_is_on ) ) {
3696 return $this->_is_on;
3697 }
3698
3699 // If already installed or pending then sure it's on :)
3700 if ( $this->is_registered() || $this->is_pending_activation() ) {
3701 $this->_is_on = true;
3702
3703 return true;
3704 }
3705
3706 return false;
3707 }
3708
3709 /**
3710 * @author Vova Feldman (@svovaf)
3711 * @since 1.1.7.3
3712 *
3713 * @param bool $flush_if_no_connectivity
3714 *
3715 * @return bool
3716 */
3717 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3718 if ( ! isset( $this->_storage->connectivity_test ) ) {
3719 // Connectivity test was never executed, or cache was cleared.
3720 return true;
3721 }
3722
3723 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3724 if ( WP_FS__IS_HTTP_REQUEST ) {
3725 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3726 // Domain changed.
3727 return true;
3728 }
3729
3730 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3731 // Server IP changed.
3732 return true;
3733 }
3734 }
3735 }
3736
3737 if ( $this->_storage->connectivity_test['is_connected'] &&
3738 $this->_storage->connectivity_test['is_active']
3739 ) {
3740 // API connected and Freemius is active - no need to run connectivity check.
3741 return false;
3742 }
3743
3744 if ( $flush_if_no_connectivity ) {
3745 /**
3746 * If explicitly asked to flush when no connectivity - do it only
3747 * if at least 10 sec passed from the last API connectivity test.
3748 */
3749 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3750 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3751 }
3752
3753 /**
3754 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3755 */
3756 $version = $this->get_plugin_version();
3757 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3758 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3759 return true;
3760 }
3761
3762 return false;
3763 }
3764
3765 /**
3766 * @author Leo Fajardo (@leorw)
3767 * @since 2.5.4
3768 *
3769 * @param bool $is_update
3770 *
3771 * @return bool
3772 */
3773 private function should_turn_fs_on( $is_update = true ) {
3774 if (
3775 empty( $this->_plugin->opt_in_moderation ) ||
3776 ! is_array( $this->_plugin->opt_in_moderation )
3777 ) {
3778 return true;
3779 }
3780
3781 $optin_config = $this->_plugin->opt_in_moderation;
3782
3783 if (
3784 WP_FS__IS_LOCALHOST &&
3785 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
3786 ) {
3787 return true;
3788 }
3789
3790 $optin_config_key = $is_update ?
3791 'updates' :
3792 'new';
3793
3794 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
3795 return true;
3796 }
3797
3798 $visibility_percentage = $optin_config[ $optin_config_key ];
3799
3800 if ( 0 == $visibility_percentage ) {
3801 return false;
3802 }
3803
3804 if ( ! is_numeric( $visibility_percentage ) ) {
3805 return true;
3806 }
3807
3808 $min = 1;
3809 $max = 100;
3810
3811 if ( function_exists( 'random_int' ) ) {
3812 $random = random_int( $min, $max ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound
3813 } else {
3814 $random = rand( $min, $max );
3815 }
3816
3817 return ( $random <= $visibility_percentage );
3818 }
3819
3820 /**
3821 * Check if there's any connectivity issue to Freemius API.
3822 *
3823 * @author Vova Feldman (@svovaf)
3824 * @since 1.0.9
3825 *
3826 * @param bool $flush_if_no_connectivity
3827 *
3828 * @return bool|null
3829 */
3830 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3831 $this->_logger->entrance();
3832
3833 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3834 return $this->_has_api_connection;
3835 }
3836
3837 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3838 isset( $this->_storage->connectivity_test ) &&
3839 true === $this->_storage->connectivity_test['is_connected']
3840 ) {
3841 $this->clear_connectivity_info();
3842 }
3843
3844 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3845 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3846 /**
3847 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3848 *
3849 * @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.
3850 */
3851 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3852 $this->is_premium() ||
3853 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3854
3855 return $this->_has_api_connection;
3856 }
3857
3858 if (
3859 ! empty( $this->_storage->connectivity_test ) &&
3860 isset( $this->_storage->connectivity_test['is_active'] )
3861 ) {
3862 $is_connected = isset( $this->_storage->connectivity_test['is_connected'] ) ?
3863 $this->_storage->connectivity_test['is_connected'] :
3864 null;
3865 $is_active = ( $this->_storage->connectivity_test['is_active'] || is_object( $this->_site ) );
3866 } else {
3867 $is_connected = null;
3868 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
3869 }
3870
3871 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), $is_connected );
3872
3873 if ( $is_active ) {
3874 $this->_is_on = true;
3875 }
3876
3877 return $this->_has_api_connection;
3878 }
3879
3880 /**
3881 * @author Leo Fajardo (@leorw)
3882 * @since 2.5.4
3883 */
3884 private function clear_connectivity_info() {
3885 unset( $this->_storage->connectivity_test );
3886
3887 FS_Api::clear_force_http_flag();
3888 }
3889
3890 /**
3891 * @author Vova Feldman (@svovaf)
3892 * @since 1.1.7.4
3893 *
3894 * @param object $pong
3895 * @param bool|null $is_connected
3896 */
3897 private function store_connectivity_info( $pong, $is_connected ) {
3898 $this->_logger->entrance();
3899
3900 $version = $this->get_plugin_version();
3901
3902 if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3903 $is_active = false;
3904 } else {
3905 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3906 }
3907
3908 $is_active = $this->apply_filters(
3909 'is_on',
3910 $is_active,
3911 $this->is_plugin_update(),
3912 $version
3913 );
3914
3915 $this->_storage->connectivity_test = array(
3916 'is_connected' => $is_connected,
3917 'host' => $_SERVER['HTTP_HOST'],
3918 'server_ip' => WP_FS__REMOTE_ADDR,
3919 'is_active' => $is_active,
3920 'timestamp' => WP_FS__SCRIPT_START_TIME,
3921 // Last version with connectivity attempt.
3922 'version' => $version,
3923 );
3924
3925 $this->_has_api_connection = $is_connected;
3926 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3927 }
3928
3929 /**
3930 * @author Leo Fajardo (@leorw)
3931 * @since 2.5.4
3932 *
3933 * @param bool $is_connected
3934 */
3935 private function update_connectivity_info( $is_connected ) {
3936 $this->store_connectivity_info(
3937 // This is true since we update the connection info only after a successful opt-in or license activation which means that Freemius has already been on even before the process.
3938 (object) array( 'is_active' => true ),
3939 $is_connected
3940 );
3941 }
3942
3943 /**
3944 * Force turning Freemius on.
3945 *
3946 * @author Vova Feldman (@svovaf)
3947 * @since 1.1.8.1
3948 *
3949 * @return bool TRUE if successfully turned on.
3950 */
3951 private function turn_on() {
3952 $this->_logger->entrance();
3953
3954 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3955 return false;
3956 }
3957
3958 $updated_connectivity = $this->_storage->connectivity_test;
3959 $updated_connectivity['is_active'] = true;
3960 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3961 $this->_storage->connectivity_test = $updated_connectivity;
3962
3963 $this->_is_on = true;
3964
3965 return true;
3966 }
3967
3968 /**
3969 * Anonymous and unique site identifier (Hash).
3970 *
3971 * @author Vova Feldman (@svovaf)
3972 * @since 1.1.0
3973 *
3974 * @param null|int $blog_id Since 2.0.0
3975 *
3976 * @return string
3977 */
3978 function get_anonymous_id( $blog_id = null ) {
3979 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
3980
3981 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
3982 $key = self::get_unfiltered_site_url( $blog_id, true );
3983
3984 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
3985 if ( empty( $secure_auth ) ||
3986 false !== strpos( $secure_auth, ' ' ) ||
3987 'put your unique phrase here' === $secure_auth
3988 ) {
3989 // Protect against default auth key.
3990 $secure_auth = md5( microtime() );
3991 }
3992
3993 /**
3994 * Base the unique identifier on the WP secure authentication key. Which
3995 * turns the key into a secret anonymous identifier. This will help us
3996 * to avoid duplicate installs generation on the backend upon opt-in.
3997 *
3998 * @author Vova Feldman (@svovaf)
3999 * @since 1.2.3
4000 */
4001 $unique_id = md5( $key . $secure_auth );
4002
4003 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4004 }
4005
4006 $this->_logger->departure( $unique_id );
4007
4008 return $unique_id;
4009 }
4010
4011 /**
4012 * Returns anonymous network ID.
4013 *
4014 * @since 2.4.3
4015 *
4016 * @return string
4017 */
4018 function get_anonymous_network_id() {
4019 return $this->get_anonymous_id( get_network()->site_id );
4020 }
4021
4022 /**
4023 * @author Vova Feldman (@svovaf)
4024 * @since 1.1.7.4
4025 *
4026 * @return \WP_User
4027 */
4028 static function _get_current_wp_user() {
4029 self::require_pluggable_essentials();
4030 self::wp_cookie_constants();
4031
4032 return wp_get_current_user();
4033 }
4034
4035 /**
4036 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4037 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4038 * is network activated the cookie constants are only configured after the network
4039 * plugins activation, therefore, if we don't define those constants WP will throw
4040 * PHP warnings/notices.
4041 *
4042 * @author Vova Feldman (@svovaf)
4043 * @since 2.1.1
4044 */
4045 private static function wp_cookie_constants() {
4046 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4047 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4048 ) {
4049 return;
4050 }
4051
4052 /**
4053 * Used to guarantee unique hash cookies
4054 *
4055 * @since 1.5.0
4056 */
4057 if ( ! defined( 'COOKIEHASH' ) ) {
4058 $siteurl = get_site_option( 'siteurl' );
4059 if ( $siteurl ) {
4060 define( 'COOKIEHASH', md5( $siteurl ) );
4061 } else {
4062 define( 'COOKIEHASH', '' );
4063 }
4064 }
4065
4066 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4067 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4068 }
4069
4070 /**
4071 * @since 2.5.0
4072 */
4073 if ( ! defined( 'AUTH_COOKIE' ) ) {
4074 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4075 }
4076
4077 /**
4078 * @since 2.6.0
4079 */
4080 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4081 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4082 }
4083 }
4084
4085 /**
4086 * @author Vova Feldman (@svovaf)
4087 * @since 2.1.0
4088 *
4089 * @return int
4090 */
4091 static function get_current_wp_user_id() {
4092 $wp_user = self::_get_current_wp_user();
4093
4094 return $wp_user->ID;
4095 }
4096
4097 /**
4098 * @author Vova Feldman (@svovaf)
4099 * @since 1.2.1.7
4100 *
4101 * @param string $email
4102 *
4103 * @return bool
4104 */
4105 static function is_valid_email( $email ) {
4106 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4107 return false;
4108 }
4109
4110 $parts = explode( '@', $email );
4111
4112 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4113 return false;
4114 }
4115
4116 $blacklist = array(
4117 'admin.',
4118 'webmaster.',
4119 'localhost.',
4120 'dev.',
4121 'development.',
4122 'test.',
4123 'stage.',
4124 'staging.',
4125 );
4126
4127 // Make sure domain is not one of the blacklisted.
4128 foreach ( $blacklist as $invalid ) {
4129 if ( 0 === strpos( $parts[1], $invalid ) ) {
4130 return false;
4131 }
4132 }
4133
4134 // Get the UTF encoded domain name.
4135 /**
4136 * @note - The check of `defined('...')` is there to account for PHP servers compiled with some older version of ICU where the constants are not defined.
4137 * @author - @swashata
4138 */
4139 $is_new_idn_available = (
4140 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4141 defined( 'IDNA_DEFAULT' ) &&
4142 defined( 'INTL_IDNA_VARIANT_UTS46' )
4143 );
4144 if ( $is_new_idn_available ) {
4145 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4146 } else {
4147 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4148 }
4149
4150 $domain = $domain . '.';
4151
4152 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4153 }
4154
4155 #endregion
4156
4157 #----------------------------------------------------------------------------------
4158 #region Email
4159 #----------------------------------------------------------------------------------
4160
4161 /**
4162 * Generates and sends an HTML email with customizable sections.
4163 *
4164 * @author Leo Fajardo (@leorw)
4165 * @since 1.1.2
4166 *
4167 * @param string $to_address
4168 * @param string $subject
4169 * @param array $sections
4170 * @param array $headers
4171 *
4172 * @return bool Whether the email contents were sent successfully.
4173 */
4174 private function send_email(
4175 $to_address,
4176 $subject,
4177 $sections = array(),
4178 $headers = array()
4179 ) {
4180 $default_sections = $this->get_email_sections();
4181
4182 // Insert new sections or replace the default email sections.
4183 if ( is_array( $sections ) && ! empty( $sections ) ) {
4184 foreach ( $sections as $section_id => $custom_section ) {
4185 if ( ! isset( $default_sections[ $section_id ] ) ) {
4186 // If the section does not exist, add it.
4187 $default_sections[ $section_id ] = $custom_section;
4188 } else {
4189 // If the section already exists, override it.
4190 $current_section = $default_sections[ $section_id ];
4191
4192 // Replace the current section's title if a custom section title exists.
4193 if ( isset( $custom_section['title'] ) ) {
4194 $current_section['title'] = $custom_section['title'];
4195 }
4196
4197 // Insert new rows under the current section or replace the default rows.
4198 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4199 foreach ( $custom_section['rows'] as $row_id => $row ) {
4200 $current_section['rows'][ $row_id ] = $row;
4201 }
4202 }
4203
4204 $default_sections[ $section_id ] = $current_section;
4205 }
4206 }
4207 }
4208
4209 $vars = array( 'sections' => $default_sections );
4210 $message = fs_get_template( 'email.php', $vars );
4211
4212 // Set the type of email to HTML.
4213 $headers[] = 'Content-type: text/html; charset=UTF-8';
4214
4215 $header_string = implode( "\r\n", $headers );
4216
4217 return wp_mail(
4218 $to_address,
4219 $subject,
4220 $message,
4221 $header_string
4222 );
4223 }
4224
4225 /**
4226 * Generates the data for the sections of the email content.
4227 *
4228 * @author Leo Fajardo (@leorw)
4229 * @since 1.1.2
4230 *
4231 * @return array
4232 */
4233 private function get_email_sections() {
4234 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4235 $current_user = self::_get_current_wp_user();
4236
4237 // Retrieve the cURL version information so that we can get the version number below.
4238 $curl_version_information = curl_version();
4239
4240 $active_plugin = self::get_active_plugins();
4241
4242 // Generate the list of active plugins separated by new line.
4243 $active_plugin_string = '';
4244 foreach ( $active_plugin as $plugin ) {
4245 $active_plugin_string .= sprintf(
4246 '<a href="%s">%s</a> [v%s]<br>',
4247 $plugin['PluginURI'],
4248 $plugin['Name'],
4249 $plugin['Version']
4250 );
4251 }
4252
4253 $server_ip = WP_FS__REMOTE_ADDR;
4254
4255 // Add PHP info for deeper investigation.
4256 ob_start();
4257 phpinfo();
4258 $php_info = ob_get_clean();
4259
4260 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4261
4262 // Generate the default email sections.
4263 $sections = array(
4264 'sdk' => array(
4265 'title' => 'SDK',
4266 'rows' => array(
4267 'fs_version' => array( 'FS Version', $this->version ),
4268 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4269 )
4270 ),
4271 'plugin' => array(
4272 'title' => ucfirst( $this->get_module_type() ),
4273 'rows' => array(
4274 'name' => array( 'Name', $this->get_plugin_name() ),
4275 'version' => array( 'Version', $this->get_plugin_version() )
4276 )
4277 ),
4278 'api' => array(
4279 'title' => 'API Subdomain',
4280 'rows' => array(
4281 'dns' => array(
4282 'DNS_CNAME',
4283 function_exists( 'dns_get_record' ) ?
4284 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4285 'dns_get_record() disabled/blocked'
4286 ),
4287 'ip' => array(
4288 'IP',
4289 function_exists( 'gethostbyname' ) ?
4290 gethostbyname( $api_domain ) :
4291 'gethostbyname() disabled/blocked'
4292 ),
4293 ),
4294 ),
4295 'site' => array(
4296 'title' => 'Site',
4297 'rows' => array(
4298 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4299 'address' => array( 'Address', site_url() ),
4300 'host' => array(
4301 'HTTP_HOST',
4302 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4303 ),
4304 'hosting' => array(
4305 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4306 fs_request_get( 'hosting_company' ) :
4307 'Unknown',
4308 ),
4309 'server_addr' => array(
4310 'SERVER_ADDR',
4311 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4312 )
4313 )
4314 ),
4315 'user' => array(
4316 'title' => 'User',
4317 'rows' => array(
4318 'email' => array( 'Email', $current_user->user_email ),
4319 'first' => array( 'First', $current_user->user_firstname ),
4320 'last' => array( 'Last', $current_user->user_lastname )
4321 )
4322 ),
4323 'plugins' => array(
4324 'title' => 'Plugins',
4325 'rows' => array(
4326 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4327 )
4328 ),
4329 'php_info' => array(
4330 'title' => 'PHP Info',
4331 'rows' => array(
4332 'info' => array( $php_info )
4333 ),
4334 )
4335 );
4336
4337 // Allow the sections to be modified by other code.
4338 $sections = $this->apply_filters( 'email_template_sections', $sections );
4339
4340 return $sections;
4341 }
4342
4343 #endregion
4344
4345 #----------------------------------------------------------------------------------
4346 #region Initialization
4347 #----------------------------------------------------------------------------------
4348
4349 /**
4350 * Init plugin's Freemius instance.
4351 *
4352 * @author Vova Feldman (@svovaf)
4353 * @since 1.0.1
4354 *
4355 * @param number $id
4356 * @param string $public_key
4357 * @param bool $is_live
4358 * @param bool $is_premium
4359 */
4360 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4361 $this->_logger->entrance();
4362
4363 $this->dynamic_init( array(
4364 'id' => $id,
4365 'public_key' => $public_key,
4366 'is_live' => $is_live,
4367 'is_premium' => $is_premium,
4368 ) );
4369 }
4370
4371 /**
4372 * Dynamic initiator, originally created to support initiation
4373 * with parent_id for add-ons.
4374 *
4375 * @author Vova Feldman (@svovaf)
4376 * @since 1.0.6
4377 *
4378 * @param array $plugin_info
4379 *
4380 * @throws Freemius_Exception
4381 */
4382 function dynamic_init( array $plugin_info ) {
4383 $this->_logger->entrance();
4384
4385 $this->parse_settings( $plugin_info );
4386
4387 $this->register_after_settings_parse_hooks();
4388
4389 /**
4390 * If anonymous but there's already a user entity and the user's site is associated with a valid license or trial period, update the anonymous mode accordingly.
4391 *
4392 * @todo Remove this entire `if` block after several releases as starting from this version, the anonymous mode will already be updated accordingly after a purchase.
4393 */
4394 if ( $this->is_anonymous() ) {
4395 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4396
4397 if (
4398 ! $is_network_level ||
4399 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4400 ) {
4401 if ( $this->is_paying_or_trial() ) {
4402 $this->reset_anonymous_mode( $is_network_level );
4403 }
4404 } else {
4405 $network = get_network();
4406
4407 if ( is_object( $network ) ) {
4408 $main_blog_id = $network->site_id;
4409 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4410
4411 if ( is_object( $first_install ) ) {
4412 $this->_storage->network_install_blog_id = $main_blog_id;
4413 $this->_storage->network_user_id = $first_install->user_id;
4414 }
4415 }
4416 }
4417 }
4418
4419 if ( $this->should_stop_execution() ) {
4420 return;
4421 }
4422
4423 if ( ! $this->is_registered() ) {
4424 if ( $this->is_anonymous() ) {
4425 // If user skipped, no need to test connectivity.
4426 $this->_has_api_connection = true;
4427 $this->_is_on = true;
4428 } else {
4429 if ( false === $this->has_api_connectivity() ) {
4430 return;
4431 } else {
4432 if ( $this->_anonymous_mode ) {
4433 // Simulate anonymous mode.
4434 $this->_is_anonymous = true;
4435 }
4436 }
4437 }
4438 }
4439
4440 /**
4441 * This should be executed even if Freemius is off for the core module,
4442 * otherwise, the add-ons dialog box won't work properly. This is especially
4443 * relevant when the developer decided to turn FS off for existing users.
4444 *
4445 * @author Vova Feldman (@svovaf)
4446 */
4447 if ( $this->is_user_in_admin() &&
4448 'plugin-information' === fs_request_get( 'tab', false ) &&
4449 $this->should_use_freemius_updater_and_dialog() &&
4450 (
4451 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4452 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4453 )
4454 ) {
4455 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4456
4457 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4458 }
4459
4460 // Check if Freemius is on for the current plugin.
4461 // This MUST be executed after all the plugin variables has been loaded.
4462 if ( ! $this->is_registered() && ! $this->is_on() ) {
4463 return;
4464 }
4465
4466 if ( $this->has_api_connectivity() ) {
4467 if ( self::is_cron() ) {
4468 $this->hook_callback_to_sync_cron();
4469 } else if ( $this->is_user_in_admin() ) {
4470 /**
4471 * Schedule daily data sync cron if:
4472 *
4473 * 1. User opted-in (for tracking).
4474 * 2. If skipped, but later upgraded (opted-in via upgrade).
4475 *
4476 * @author Vova Feldman (@svovaf)
4477 * @since 1.1.7.3
4478 *
4479 */
4480 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4481 $this->maybe_schedule_sync_cron();
4482 }
4483
4484 /**
4485 * Check if requested for manual blocking background sync.
4486 */
4487 if ( fs_request_has( 'background_sync' ) ) {
4488 self::require_pluggable_essentials();
4489 self::wp_cookie_constants();
4490
4491 $this->run_manual_sync();
4492 }
4493 }
4494 }
4495
4496 if ( $this->is_registered() ) {
4497 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4498
4499 $this->hook_callback_to_install_sync();
4500 }
4501
4502 if ( $this->is_addon() ) {
4503 if ( $this->is_parent_plugin_installed() ) {
4504 // Link to parent FS.
4505 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4506
4507 // Get parent plugin reference.
4508 $this->_parent_plugin = $this->_parent->get_plugin();
4509 }
4510 }
4511
4512 if ( $this->is_user_in_admin() ) {
4513 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4514 $this->_admin_notices->add_sticky(
4515 sprintf(
4516 /* translators: %s: License type (e.g. you have a professional license) */
4517 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4518 fs_request_get( 'purchased_plan' )
4519 ) .
4520 sprintf(
4521 $this->get_text_inline(" The %s's %sdownload link%s, license key, and installation instructions have been sent to %s. If you can't find the email after 5 min, please check your spam box.", 'post-purchase-email-sent-message' ),
4522 $this->get_module_label( true ),
4523 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4524 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4525 sprintf(
4526 '<strong>%s</strong>',
4527 fs_request_get( 'purchase_email' )
4528 )
4529 ),
4530 'plan_purchased',
4531 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4532 );
4533 }
4534
4535 if ( $this->is_addon() ) {
4536 if ( ! $this->is_parent_plugin_installed() ) {
4537 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4538
4539 if ( isset( $plugin_info['parent'] ) ) {
4540 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4541 }
4542
4543 $this->_admin_notices->add(
4544 ( ! empty( $parent_name ) ?
4545 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 ) :
4546 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() )
4547 ),
4548 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4549 'error'
4550 );
4551
4552 return;
4553 } else {
4554 $is_network_admin = fs_is_network_admin();
4555
4556 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4557 // If add-on activated and parent not, automatically install parent for the user.
4558 $this->activate_parent_account( $this->_parent );
4559 } else if (
4560 $this->_parent->is_registered() &&
4561 ! $this->is_registered() &&
4562 /**
4563 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4564 * * Network active and in network admin - network activate add-on account.
4565 * * Network active and not in network admin - activate add-on account for the current blog.
4566 * * Not network active and not in network admin - activate add-on account for the current blog.
4567 *
4568 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4569 *
4570 * @author Leo Fajardo (@leorw)
4571 * @since 2.3.0
4572 */
4573 ( $this->is_network_active() || ! $is_network_admin )
4574 ) {
4575 $premium_license = null;
4576
4577 if (
4578 ! $this->has_free_plan() &&
4579 $this->is_bundle_license_auto_activation_enabled() &&
4580 $this->_parent->is_activated_with_bundle_license()
4581 ) {
4582 /**
4583 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4584 *
4585 * @author Leo Fajardo (@leorw)
4586 * @since 2.4.0
4587 */
4588 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4589
4590 if (
4591 is_object( $bundle_license ) &&
4592 ! empty( $bundle_license->products ) &&
4593 in_array( $this->get_id(), $bundle_license->products )
4594 ) {
4595 $premium_license = $bundle_license;
4596 }
4597 }
4598
4599 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4600 // If parent plugin activated, automatically install add-on for the user.
4601 $this->_activate_addon_account(
4602 $this->_parent,
4603 ( $this->is_network_active() && $is_network_admin ) ?
4604 true :
4605 get_current_blog_id(),
4606 $premium_license
4607 );
4608 }
4609 }
4610
4611 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4612 if ( $this->is_premium() ) {
4613 // Remove add-on download admin-notice.
4614 $this->_parent->_admin_notices->remove_sticky( array(
4615 'addon_plan_upgraded_' . $this->_slug,
4616 'no_addon_license_' . $this->_slug,
4617 ) );
4618 }
4619
4620 // $this->deactivate_premium_only_addon_without_license();
4621 }
4622 }
4623
4624 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4625
4626 // if ( $this->is_registered() ||
4627 // $this->is_anonymous() ||
4628 // $this->is_pending_activation()
4629 // ) {
4630 // $this->_init_admin();
4631 // }
4632 }
4633
4634 /**
4635 * Should be called outside `$this->is_user_in_admin()` scope
4636 * because the updater has some logic that needs to be executed
4637 * during AJAX calls.
4638 *
4639 * Currently, we need to hook to the `http_request_host_is_external` filter.
4640 * In the future, there might be additional logic added.
4641 *
4642 * @author Vova Feldman
4643 * @since 1.2.1.6
4644 */
4645 if (
4646 $this->should_use_freemius_updater_and_dialog() &&
4647 (
4648 $this->is_premium() ||
4649 /**
4650 * If not premium but the premium version is installed, also instantiate the updater so that the
4651 * plugin information dialog of the premium version will have the information from the server.
4652 *
4653 * @author Leo Fajardo (@leorw)
4654 * @since 2.2.3
4655 */
4656 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4657 ) &&
4658 $this->has_release_on_freemius() &&
4659 ( ! $this->is_unresolved_clone( true ) )
4660 ) {
4661 FS_Plugin_Updater::instance( $this );
4662 }
4663
4664 $this->do_action( 'initiated' );
4665
4666 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4667 if ( isset( $this->_storage->prev_is_premium ) ) {
4668 $this->apply_filters(
4669 'after_code_type_change',
4670 // New code type.
4671 $this->_plugin->is_premium
4672 );
4673 } else {
4674 // Set for code type for the first time.
4675 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4676 }
4677 }
4678
4679 if ( ! $this->is_addon() ) {
4680 if ( $this->is_registered() ) {
4681 // Fix for upgrade from versions < 1.0.9.
4682 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4683 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4684 }
4685
4686 $this->do_action( 'after_init_plugin_registered' );
4687 } else if ( $this->is_anonymous() ) {
4688 $this->do_action( 'after_init_plugin_anonymous' );
4689 } else if ( $this->is_pending_activation() ) {
4690 $this->do_action( 'after_init_plugin_pending_activations' );
4691 }
4692 } else {
4693 if ( $this->is_registered() ) {
4694 $this->do_action( 'after_init_addon_registered' );
4695 } else if ( $this->is_anonymous() ) {
4696 $this->do_action( 'after_init_addon_anonymous' );
4697 } else if ( $this->is_pending_activation() ) {
4698 $this->do_action( 'after_init_addon_pending_activations' );
4699 }
4700 }
4701 }
4702
4703 /**
4704 * @author Leo Fajardo (@leorw)
4705 * @since 2.2.3
4706 *
4707 * @return bool
4708 */
4709 private function should_use_freemius_updater_and_dialog() {
4710 return (
4711 /**
4712 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
4713 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
4714 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
4715 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
4716 * plugin details from .org).
4717 */
4718 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
4719 (
4720 ! self::is_plugin_install_page() &&
4721 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
4722 ( 'install-plugin' !== fs_request_get( 'action' ) )
4723 )
4724 );
4725 }
4726
4727 /**
4728 * @param string[] $permissions
4729 * @param bool $is_enabled
4730 * @param int|null $blog_id
4731 *
4732 * @return true|object `true` on success, API error object on failure.
4733 */
4734 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
4735 $this->_logger->entrance();
4736
4737 $params = array(
4738 'permissions' => implode( ',', $permissions ),
4739 'is_enabled' => $is_enabled,
4740 );
4741
4742 $current_blog_id = get_current_blog_id();
4743 $is_blog_switched = false;
4744 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
4745 $is_blog_switched = $this->switch_to_blog( $blog_id );
4746 }
4747
4748 $result = $this->api_site_call( '/permissions.json', 'put', $params );
4749
4750 if ( $is_blog_switched ) {
4751 $this->switch_to_blog( $current_blog_id );
4752 }
4753
4754 if (
4755 ! $this->is_api_result_object( $result ) ||
4756 ! isset( $result->install_id )
4757 ) {
4758 $this->_logger->api_error( $result );
4759
4760 return $result;
4761 }
4762
4763 return true;
4764 }
4765
4766 /**
4767 * @param string[] $permissions
4768 * @param bool $is_enabled
4769 * @param bool $has_site_delegated_connection
4770 *
4771 * @return true|object `true` on success, API error object on failure.
4772 */
4773 private function update_network_permissions(
4774 array $permissions,
4775 $is_enabled,
4776 &$has_site_delegated_connection
4777 ) {
4778 $this->_logger->entrance();
4779
4780 $install_id_2_blog_id = array();
4781 $install_by_blog_id = $this->get_blog_install_map();
4782
4783 $has_site_delegated_connection = false;
4784
4785 foreach ( $install_by_blog_id as $blog_id => $install ) {
4786 if ( $this->is_site_delegated_connection( $blog_id ) ) {
4787 // Only update permissions of non-delegated installs.
4788 $has_site_delegated_connection = true;
4789 continue;
4790 }
4791
4792 $install_id_2_blog_id[ $install->id ] = $blog_id;
4793 }
4794
4795 if ( empty( $install_id_2_blog_id ) ) {
4796 return true;
4797 }
4798
4799 $params = array(
4800 'permissions' => implode( ',', $permissions ),
4801 'is_enabled' => $is_enabled,
4802 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
4803 );
4804
4805 // Send update to FS.
4806 $result = $this->get_current_or_network_user_api_scope()->call(
4807 "/plugins/{$this->_module_id}/installs/permissions.json",
4808 'put',
4809 $params
4810 );
4811
4812 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
4813 $this->_logger->api_error( $result );
4814
4815 return $result;
4816 }
4817
4818 return true;
4819 }
4820
4821 /**
4822 * @param mixed $result
4823 *
4824 * @return string
4825 */
4826 private function get_api_error_message( $result ) {
4827 $error_message = sprintf( $this->get_text_inline( 'There was an unexpected API error while processing your request. Please try again in a few minutes and if it still doesn\'t work, contact the %s\'s author with the following:',
4828 'unexpected-api-error' ), $this->_module_type ) . ' ';
4829
4830 if (
4831 $this->is_api_error( $result ) &&
4832 isset( $result->error )
4833 ) {
4834 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
4835
4836 $error_message .= "<b>{$result->error->message}{$code}</b>";
4837 } else {
4838 $error_message .= var_export( $result, true );
4839 }
4840
4841 return $error_message;
4842 }
4843
4844 /**
4845 * @author Vova Feldman (@svovaf)
4846 * @since 2.5.1
4847 */
4848 function _toggle_permission_tracking_callback() {
4849 $this->_logger->entrance();
4850
4851 $this->check_ajax_referer( 'toggle_permission_tracking' );
4852
4853 if ( ! $this->is_registered( true ) ) {
4854 self::shoot_ajax_failure( 'User never opted-in.' );
4855 }
4856
4857 $is_enabled = fs_request_get_bool( 'is_enabled' );
4858 $permissions = fs_request_get( 'permissions' );
4859
4860 if ( ! is_string( $permissions ) ) {
4861 self::shoot_ajax_failure( 'The permissions param must be a string.' );
4862 }
4863
4864 $permissions = explode( ',', $permissions );
4865
4866 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
4867
4868 if ( true !== $result ) {
4869 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
4870 }
4871
4872 self::shoot_ajax_success();
4873 }
4874
4875 /**
4876 * @param string[] $permissions
4877 * @param bool $is_enabled
4878 * @param int|null $blog_id
4879 *
4880 * @return bool|mixed `true` if updated successfully or no update is needed.
4881 */
4882 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
4883 if ( ! $this->is_registered( true ) ) {
4884 // User never opted-in.
4885 return false;
4886 }
4887
4888 // Check if permissions are already set as needed.
4889 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
4890 /**
4891 * Note:
4892 * When running on the network admin, there's no need to iterate through all the installs individually since network opt-in permissions are managed for ALL non-delegated installs through a single option (per permission) on the network-level storage.
4893 */
4894 return true;
4895 }
4896
4897 $api_managed_permissions = array_intersect(
4898 $permissions,
4899 FS_Permission_Manager::get_api_managed_permission_ids()
4900 );
4901
4902 if (
4903 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
4904 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
4905 ) {
4906 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
4907 }
4908
4909 if ( ! empty( $api_managed_permissions ) ) {
4910 $has_site_delegated_connection = false;
4911
4912 if (
4913 ! $is_enabled &&
4914 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
4915 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
4916 ) {
4917 /**
4918 * If we are turning off a permission and the extensions permission is off too, enrich the permissions update request to also turn off extensions tracking, as currently when opting in with extensions tracking disabled the extensions tracking is off but the API isn't aware of it.
4919 *
4920 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
4921 */
4922 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
4923 }
4924
4925 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
4926 $result = $this->update_network_permissions(
4927 $api_managed_permissions,
4928 $is_enabled,
4929 $has_site_delegated_connection
4930 );
4931 } else {
4932 $result = $this->update_site_permissions(
4933 $api_managed_permissions,
4934 $is_enabled,
4935 $blog_id
4936 );
4937 }
4938
4939 if ( true !== $result ) {
4940 return $result;
4941 }
4942
4943 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
4944 if ( $is_enabled ) {
4945 $this->schedule_sync_cron();
4946 } else {
4947 $this->clear_sync_cron( ! $has_site_delegated_connection );
4948 }
4949 }
4950
4951 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
4952 $this->toggle_user_permission( $is_enabled, $blog_id );
4953 }
4954 }
4955
4956 $this->update_tracking_permissions(
4957 $permissions,
4958 $is_enabled,
4959 $blog_id
4960 );
4961
4962 return true;
4963 }
4964
4965 /**
4966 * @param bool $is_enabled
4967 * @param int|null $blog_id
4968 */
4969 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
4970 $network_or_blog_ids = is_numeric( $blog_id ) ?
4971 $blog_id :
4972 fs_is_network_admin();
4973
4974 if ( $is_enabled ) {
4975 $this->reset_anonymous_mode( $network_or_blog_ids );
4976 } else {
4977 $this->skip_connection( $network_or_blog_ids );
4978 }
4979 }
4980
4981 /**
4982 * Opt-in back into usage tracking.
4983 *
4984 * Note: This will only work if the user opted-in previously.
4985 *
4986 * Returns:
4987 * 1. FALSE - If the user never opted-in.
4988 * 2. TRUE - If successfully opted-in back to usage tracking.
4989 * 3. object - API result on failure.
4990 *
4991 * @author Leo Fajardo (@leorw)
4992 * @since 1.2.1.5
4993 *
4994 * @bool $is_enabled
4995 *
4996 * @return bool|object
4997 */
4998 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
4999 $this->_logger->entrance();
5000
5001 return $this->toggle_permission_tracking(
5002 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5003 $is_enabled,
5004 $blog_id
5005 );
5006 }
5007
5008 /**
5009 * If user opted-in and later disabled usage-tracking,
5010 * re-allow tracking for licensing and updates.
5011 *
5012 * @author Leo Fajardo (@leorw)
5013 * @since 1.2.1.5
5014 *
5015 * @param bool $is_context_single_site
5016 */
5017 private function reconnect_locally( $is_context_single_site = false ) {
5018 $this->_logger->entrance();
5019
5020 if ( ! $this->is_registered() ) {
5021 return;
5022 }
5023
5024 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5025 if ( $this->is_tracking_prohibited() ) {
5026 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5027 }
5028 } else {
5029 $installs_map = $this->get_blog_install_map();
5030 foreach ( $installs_map as $blog_id => $install ) {
5031 /**
5032 * @var FS_Site $install
5033 */
5034 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5035 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5036 }
5037 }
5038 }
5039 }
5040
5041 /**
5042 * Update permission tracking flags. When updating in a network context, in addition to updating the network-level flags, also update the permissions on the site-level for all non-delegated sites.
5043 *
5044 * @param string[] $permissions
5045 * @param bool $is_enabled
5046 * @param int|null $blog_id
5047 *
5048 * @return array
5049 */
5050 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5051 // Alias.
5052 $permission_manager = FS_Permission_Manager::instance( $this );
5053
5054 $network_or_blog_ids = is_numeric( $blog_id ) ?
5055 $blog_id :
5056 fs_is_network_admin();
5057
5058 if ( true === $network_or_blog_ids ) {
5059 // Update the permission for all non-delegated sub-sites.
5060 $blog_ids = $this->get_non_delegated_blog_ids();
5061
5062 // Add the network-level to the array, to update the permission on the network-level storage.
5063 array_unshift( $blog_ids, null );
5064 }
5065 else
5066 {
5067 if ( false === $network_or_blog_ids ) {
5068 $network_or_blog_ids = null;
5069 }
5070
5071 $blog_ids = is_array( $network_or_blog_ids ) ?
5072 $network_or_blog_ids :
5073 array( $network_or_blog_ids );
5074 }
5075
5076 $result = array();
5077 foreach ( $permissions as $permission ) {
5078 $permission = trim( $permission );
5079 $is_permission_supported = true;
5080
5081 foreach ( $blog_ids as $id ) {
5082 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5083 $permission,
5084 $is_enabled,
5085 $id
5086 );
5087 }
5088
5089 if ( ! $is_permission_supported ) {
5090 $permission = 'no_match';
5091 }
5092
5093 $result[ $permission ] = $is_enabled;
5094 }
5095
5096 return $result;
5097 }
5098
5099 /**
5100 * Parse plugin's settings (as defined by the plugin dev).
5101 *
5102 * @author Vova Feldman (@svovaf)
5103 * @since 1.1.7.3
5104 *
5105 * @param array $plugin_info
5106 *
5107 * @throws \Freemius_Exception
5108 */
5109 private function parse_settings( &$plugin_info ) {
5110 $this->_logger->entrance();
5111
5112 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5113 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5114 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5115 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5116 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5117
5118 /**
5119 * @author Vova Feldman (@svovaf)
5120 * @since 1.1.9 Try to pull secret key from external config.
5121 */
5122 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5123 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5124 }
5125
5126 if ( isset( $plugin_info['parent'] ) ) {
5127 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5128 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5129 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5130 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5131 }
5132
5133 if ( false === $id ) {
5134 throw new Freemius_Exception( array(
5135 'error' => array(
5136 'type' => 'ParameterNotSet',
5137 'message' => 'Plugin id parameter is not set.',
5138 'code' => 'plugin_id_not_set',
5139 'http' => 500,
5140 )
5141 ) );
5142 }
5143 if ( false === $public_key ) {
5144 throw new Freemius_Exception( array(
5145 'error' => array(
5146 'type' => 'ParameterNotSet',
5147 'message' => 'Plugin public_key parameter is not set.',
5148 'code' => 'plugin_public_key_not_set',
5149 'http' => 500,
5150 )
5151 ) );
5152 }
5153
5154 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5155 $this->_plugin :
5156 new FS_Plugin();
5157
5158 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5159
5160 $plugin->update( array(
5161 'id' => $id,
5162 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5163 'public_key' => $public_key,
5164 'slug' => $this->_slug,
5165 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5166 'parent_plugin_id' => $parent_id,
5167 'version' => $this->get_plugin_version(),
5168 'title' => $this->get_plugin_name( $premium_suffix ),
5169 'file' => $this->_plugin_basename,
5170 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5171 'premium_suffix' => $premium_suffix,
5172 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5173 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5174 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5175 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5176 'opt_in_moderation' => $this->get_option(
5177 $plugin_info,
5178 'opt_in',
5179 // For backward compatibility, we support both parameter names: opt_in and opt_in_moderation.
5180 $this->get_option( $plugin_info, 'opt_in_moderation', null )
5181 ),
5182 ) );
5183
5184 if ( $plugin->is_updated() ) {
5185 // Update plugin details.
5186 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5187 }
5188 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5189 $this->_plugin->secret_key = $secret_key;
5190
5191 /**
5192 * 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).
5193 *
5194 * @author Vova Feldman
5195 * @since 2.4.5
5196 */
5197 if ( $this->is_network_active() && fs_is_network_admin() ) {
5198 if ( isset( $plugin_info['menu_network'] ) &&
5199 is_array( $plugin_info['menu_network'] ) &&
5200 ! empty( $plugin_info['menu_network'] )
5201 ) {
5202 $plugin_info['menu'] = $plugin_info['menu_network'];
5203 }
5204 }
5205
5206 if ( ! isset( $plugin_info['menu'] ) ) {
5207 $plugin_info['menu'] = array();
5208
5209 if ( ! empty( $this->_storage->sdk_last_version ) &&
5210 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5211 ) {
5212 // Backward compatibility to 1.1.2
5213 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5214 $plugin_info['menu_slug'] :
5215 $this->_slug;
5216 }
5217 }
5218
5219 $this->_menu = FS_Admin_Menu_Manager::instance(
5220 $this->_module_id,
5221 $this->_module_type,
5222 $this->get_unique_affix()
5223 );
5224
5225 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5226
5227 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5228 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5229 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5230 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5231 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5232 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5233 if ( $this->_is_premium_only ) {
5234 // If premium only plugin, disable anonymous mode.
5235 $this->_enable_anonymous = false;
5236 $this->_anonymous_mode = false;
5237 } else {
5238 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5239 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5240 }
5241 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5242 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5243
5244 if ( ! empty( $plugin_info['trial'] ) ) {
5245 $this->_trial_days = $this->get_numeric_option(
5246 $plugin_info['trial'],
5247 'days',
5248 // Default to 0 - trial without days specification.
5249 0
5250 );
5251
5252 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5253 }
5254
5255 $this->_navigation = $this->get_option(
5256 $plugin_info,
5257 'navigation',
5258 $this->is_free_wp_org_theme() ?
5259 self::NAVIGATION_TABS :
5260 self::NAVIGATION_MENU
5261 );
5262 }
5263
5264 /**
5265 * @param string[] $options
5266 * @param string $key
5267 * @param mixed $default
5268 *
5269 * @return bool
5270 */
5271 private function get_option( &$options, $key, $default = false ) {
5272 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5273 }
5274
5275 private function get_bool_option( &$options, $key, $default = false ) {
5276 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5277 }
5278
5279 private function get_numeric_option( &$options, $key, $default = false ) {
5280 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5281 }
5282
5283 /**
5284 * Gate keeper.
5285 *
5286 * @author Vova Feldman (@svovaf)
5287 * @since 1.1.7.3
5288 *
5289 * @return bool
5290 */
5291 private function should_stop_execution() {
5292 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5293 /**
5294 * Don't execute Freemius until plugin was fully loaded at least once,
5295 * to give the opportunity for the activation hook to run before pinging
5296 * the API for connectivity test. This logic is relevant for the
5297 * identification of new plugin install vs. plugin update.
5298 *
5299 * @author Vova Feldman (@svovaf)
5300 * @since 1.1.9
5301 */
5302 return true;
5303 }
5304
5305 if ( $this->is_activation_mode() ) {
5306 if ( ! is_admin() ) {
5307 /**
5308 * If in activation mode, don't execute Freemius outside the admin dashboard.
5309 *
5310 * @author Vova Feldman (@svovaf)
5311 * @since 1.1.7.3
5312 */
5313 return true;
5314 }
5315
5316 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5317 /**
5318 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5319 * then don't start Freemius.
5320 *
5321 * @author Vova Feldman (@svovaf)
5322 * @since 1.1.6.3
5323 *
5324 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5325 */
5326 return true;
5327 }
5328
5329 if ( self::is_cron() ) {
5330 /**
5331 * If in activation mode, don't execute Freemius during wp crons
5332 * (wp crons have HTTP context - called as HTTP request).
5333 *
5334 * @author Vova Feldman (@svovaf)
5335 * @since 1.1.7.3
5336 */
5337 return true;
5338 }
5339
5340 if ( self::is_ajax() ) {
5341 /**
5342 * During activation, if running in AJAX mode, unless there's a sticky
5343 * connectivity issue notice, don't run Freemius.
5344 *
5345 * @author Vova Feldman (@svovaf)
5346 * @since 1.1.7.3
5347 */
5348 return true;
5349 }
5350 }
5351
5352 return false;
5353 }
5354
5355 /**
5356 * Triggered after code type has changed.
5357 *
5358 * @author Vova Feldman (@svovaf)
5359 * @since 1.1.9.1
5360 */
5361 function _after_code_type_change() {
5362 $this->_logger->entrance();
5363
5364 if ( $this->is_theme() ) {
5365 // Expire the cache of the previous tabs since the theme may
5366 // have setting updates after code type has changed.
5367 $this->_cache->expire( 'tabs' );
5368 $this->_cache->expire( 'tabs_stylesheets' );
5369 }
5370
5371 if ( ! $this->is_addon() ) {
5372 add_action(
5373 is_admin() ? 'admin_init' : 'init',
5374 array( &$this, '_plugin_code_type_changed' )
5375 );
5376 }
5377
5378 if ( $this->is_registered() && $this->is_premium() ) {
5379 // Purge cached payments after switching to the premium version.
5380 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5381 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5382 }
5383 }
5384
5385 /**
5386 * Handles plugin's code type change (free <--> premium).
5387 *
5388 * @author Vova Feldman (@svovaf)
5389 * @since 1.0.9
5390 */
5391 function _plugin_code_type_changed() {
5392 $this->_logger->entrance();
5393
5394 if ( $this->is_premium() ) {
5395 $this->reconnect_locally();
5396
5397 // Activated premium code.
5398 $this->do_action( 'after_premium_version_activation' );
5399
5400 // Remove all sticky messages related to download of the premium version.
5401 $this->_admin_notices->remove_sticky( array(
5402 'trial_started',
5403 'plan_upgraded',
5404 'plan_changed',
5405 'license_activated',
5406 ) );
5407
5408 $notice = '';
5409 if ( ! $this->is_only_premium() ) {
5410 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5411 }
5412
5413 $license_notice = $this->get_license_network_activation_notice();
5414 if ( ! empty( $license_notice ) ) {
5415 $notice .= ' ' . $license_notice;
5416 }
5417
5418 if ( ! empty( $notice ) ) {
5419 $this->_admin_notices->add_sticky(
5420 trim( $notice ),
5421 'premium_activated',
5422 $this->get_text_x_inline( 'W00t',
5423 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5424 );
5425 }
5426 } else {
5427 // Remove sticky message related to premium code activation.
5428 $this->_admin_notices->remove_sticky( 'premium_activated' );
5429
5430 // Activated free code (after had the premium before).
5431 $this->do_action( 'after_free_version_reactivation' );
5432
5433 if ( $this->is_paying() && ! $this->is_premium() ) {
5434 $this->add_complete_upgrade_instructions_notice(
5435 sprintf(
5436 /* translators: %s: License type (e.g. you have a professional license) */
5437 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5438 $this->get_plan_title()
5439 ),
5440 'plan_upgraded'
5441 );
5442 }
5443 }
5444
5445 if ( $this->is_registered() ) {
5446 // Schedule code type changes event.
5447 $this->schedule_install_sync();
5448 }
5449
5450 /**
5451 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5452 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5453 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5454 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5455 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5456 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5457 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5458 */
5459 $this->unregister_uninstall_hook();
5460
5461 $this->clear_module_main_file_cache();
5462
5463 // Update is_premium of latest version.
5464 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5465 }
5466
5467 #endregion
5468
5469 #----------------------------------------------------------------------------------
5470 #region Add-ons
5471 #----------------------------------------------------------------------------------
5472
5473 /**
5474 * Check if add-on installed and activated on site.
5475 *
5476 * @author Vova Feldman (@svovaf)
5477 * @since 1.0.6
5478 *
5479 * @param string|number $id_or_slug
5480 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5481 *
5482 * @return bool
5483 */
5484 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5485 $this->_logger->entrance();
5486
5487 $addon_id = self::get_module_id( $id_or_slug );
5488 $is_activated = self::has_instance( $addon_id );
5489
5490 if ( ! $is_activated ) {
5491 return false;
5492 }
5493
5494 if ( is_bool( $is_premium ) ) {
5495 // Check if the specified code version is activate.
5496 $addon = $this->get_addon_instance( $addon_id );
5497 $is_activated = ( $is_premium === $addon->is_premium() );
5498 }
5499
5500 return $is_activated;
5501 }
5502
5503 /**
5504 * Check if add-on was connected to install
5505 *
5506 * @author Vova Feldman (@svovaf)
5507 * @since 1.1.7
5508 *
5509 * @param string|number $id_or_slug
5510 *
5511 * @return bool
5512 */
5513 function is_addon_connected( $id_or_slug ) {
5514 $this->_logger->entrance();
5515
5516 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5517
5518 $addon_id = self::get_module_id( $id_or_slug );
5519 $addon = $this->get_addon( $addon_id );
5520 $slug = $addon->slug;
5521 if ( ! isset( $sites[ $slug ] ) ) {
5522 return false;
5523 }
5524
5525 $site = $sites[ $slug ];
5526
5527 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5528
5529 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5530 // The given slug do NOT belong to any of the plugin's add-ons.
5531 return false;
5532 }
5533
5534 return ( is_object( $site ) &&
5535 is_numeric( $site->id ) &&
5536 is_numeric( $site->user_id ) &&
5537 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5538 );
5539 }
5540
5541 /**
5542 * Determines if add-on installed.
5543 *
5544 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5545 *
5546 * @author Vova Feldman (@svovaf)
5547 * @since 1.0.6
5548 *
5549 * @param string|number $id_or_slug
5550 *
5551 * @return bool
5552 */
5553 function is_addon_installed( $id_or_slug ) {
5554 $this->_logger->entrance();
5555
5556 $addon_id = self::get_module_id( $id_or_slug );
5557
5558 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5559 }
5560
5561 /**
5562 * Get add-on basename.
5563 *
5564 * @author Vova Feldman (@svovaf)
5565 * @since 1.0.6
5566 *
5567 * @param string|number $id_or_slug
5568 *
5569 * @return string
5570 */
5571 function get_addon_basename( $id_or_slug ) {
5572 $addon_id = self::get_module_id( $id_or_slug );
5573
5574 if ( $this->is_addon_activated( $addon_id ) ) {
5575 return self::instance( $addon_id )->get_plugin_basename();
5576 }
5577
5578 $addon = $this->get_addon( $addon_id );
5579 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5580
5581 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5582 return $premium_basename;
5583 }
5584
5585 $all_plugins = $this->get_all_plugins();
5586
5587 foreach ( $all_plugins as $basename => $data ) {
5588 if ( $addon->slug === $data['slug'] ||
5589 $addon->premium_slug === $data['slug']
5590 ) {
5591 return $basename;
5592 }
5593 }
5594
5595 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5596
5597 return $free_basename;
5598 }
5599
5600 /**
5601 * Get installed add-ons instances.
5602 *
5603 * @author Vova Feldman (@svovaf)
5604 * @since 1.0.6
5605 *
5606 * @return Freemius[]
5607 */
5608 function get_installed_addons() {
5609 if ( $this->is_addon() ) {
5610 // Add-on cannot have add-ons.
5611 return array();
5612 }
5613
5614 $installed_addons = array();
5615
5616 foreach ( self::$_instances as $instance ) {
5617 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5618 $installed_addons[] = $instance;
5619 }
5620 }
5621
5622 return $installed_addons;
5623 }
5624
5625 /**
5626 * Check if any add-ons of the plugin are installed.
5627 *
5628 * @author Leo Fajardo (@leorw)
5629 * @since 1.1.1
5630 *
5631 * @return bool
5632 */
5633 function has_installed_addons() {
5634 if ( ! $this->has_addons() ) {
5635 return false;
5636 }
5637
5638 foreach ( self::$_instances as $instance ) {
5639 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5640 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5641 return true;
5642 }
5643 }
5644 }
5645
5646 return false;
5647 }
5648
5649 /**
5650 * Tell Freemius that the current plugin is an add-on.
5651 *
5652 * @author Vova Feldman (@svovaf)
5653 * @since 1.0.6
5654 *
5655 * @param number $parent_plugin_id The parent plugin ID
5656 */
5657 function init_addon( $parent_plugin_id ) {
5658 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5659 }
5660
5661 /**
5662 * @author Vova Feldman (@svovaf)
5663 * @since 1.0.6
5664 *
5665 * @return bool
5666 */
5667 function is_addon() {
5668 return (
5669 isset( $this->_plugin->parent_plugin_id ) &&
5670 is_numeric( $this->_plugin->parent_plugin_id )
5671 );
5672 }
5673
5674 /**
5675 * @author Vova Feldman (@svovaf)
5676 * @since 2.3.2
5677 *
5678 * @param number $parent_product_id
5679 *
5680 * @return bool
5681 */
5682 function is_addon_of( $parent_product_id ) {
5683 return (
5684 $this->is_addon() &&
5685 $parent_product_id == $this->_plugin->parent_plugin_id
5686 );
5687 }
5688
5689 /**
5690 * Deactivate add-on if it's premium only and the user does't have a valid license.
5691 *
5692 * @param bool $is_after_trial_cancel
5693 *
5694 * @return bool If add-on was deactivated.
5695 */
5696 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5697 if ( ! $this->has_free_plan() &&
5698 ! $this->has_features_enabled_license() &&
5699 ! $this->_has_premium_license()
5700 ) {
5701 if ( $this->is_registered() ) {
5702 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5703 // if (empty($this->_storage->activation_timestamp) ||
5704 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5705 // ) {
5706 /**
5707 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5708 *
5709 * Retry syncing the user add-on licenses.
5710 */
5711 // Sync licenses.
5712 $this->_sync_licenses();
5713 // }
5714
5715 // Try to activate premium license.
5716 $this->_activate_license( true );
5717 }
5718
5719 if ( ! $this->has_free_plan() &&
5720 ! $this->has_features_enabled_license() &&
5721 ! $this->_has_premium_license()
5722 ) {
5723 // @todo Check if deactivate plugins also call the deactivation hook.
5724
5725 $this->_parent->_admin_notices->add_sticky(
5726 sprintf(
5727 ( $is_after_trial_cancel ?
5728 $this->_parent->get_text_inline(
5729 '%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.',
5730 'addon-trial-cancelled-message'
5731 ) :
5732 $this->_parent->get_text_inline(
5733 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5734 'addon-no-license-message'
5735 )
5736 ),
5737 '<b>' . $this->_plugin->title . '</b>'
5738 ) . ' ' . sprintf(
5739 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5740 $this->_parent->addon_url( $this->_slug ),
5741 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5742 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5743 ),
5744 'no_addon_license_' . $this->_slug,
5745 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5746 ( $is_after_trial_cancel ? 'success' : 'error' )
5747 );
5748
5749 deactivate_plugins( array( $this->_plugin_basename ), true );
5750
5751 return true;
5752 }
5753 }
5754
5755 return false;
5756 }
5757
5758 #endregion
5759
5760 #----------------------------------------------------------------------------------
5761 #region Sandbox
5762 #----------------------------------------------------------------------------------
5763
5764 /**
5765 * Set Freemius into sandbox mode for debugging.
5766 *
5767 * @author Vova Feldman (@svovaf)
5768 * @since 1.0.4
5769 *
5770 * @param string $secret_key
5771 */
5772 function init_sandbox( $secret_key ) {
5773 $this->_plugin->secret_key = $secret_key;
5774
5775 // Update plugin details.
5776 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
5777 }
5778
5779 /**
5780 * Check if running payments in sandbox mode.
5781 *
5782 * @author Vova Feldman (@svovaf)
5783 * @since 1.0.4
5784 *
5785 * @return bool
5786 */
5787 function is_payments_sandbox() {
5788 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
5789 }
5790
5791 #endregion
5792
5793 /**
5794 * Check if running test vs. live plugin.
5795 *
5796 * @author Vova Feldman (@svovaf)
5797 * @since 1.0.5
5798 *
5799 * @return bool
5800 */
5801 function is_live() {
5802 return $this->_plugin->is_live;
5803 }
5804
5805 /**
5806 * Check if super-admin skipped connection for all sites in the network.
5807 *
5808 * @author Vova Feldman (@svovaf)
5809 * @since 2.0.0
5810 */
5811 function is_network_anonymous() {
5812 if ( ! $this->_is_network_active ) {
5813 return false;
5814 }
5815
5816 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
5817
5818 if ( empty( $is_anonymous_ms ) ) {
5819 return false;
5820 }
5821
5822 return $is_anonymous_ms['is'];
5823 }
5824
5825 /**
5826 * Check if super-admin opted-in for all sites in the network.
5827 *
5828 * @author Vova Feldman (@svovaf)
5829 * @since 2.0.0
5830 */
5831 function is_network_connected() {
5832 if ( ! $this->_is_network_active ) {
5833 return false;
5834 }
5835
5836 return $this->_storage->get( 'is_network_connected' );
5837 }
5838
5839 /**
5840 * Check if the user skipped connecting the account with Freemius.
5841 *
5842 * @author Vova Feldman (@svovaf)
5843 * @since 1.0.7
5844 *
5845 * @return bool
5846 */
5847 function is_anonymous() {
5848 if ( ! isset( $this->_is_anonymous ) ) {
5849 if ( $this->is_network_anonymous() ) {
5850 $this->_is_anonymous = true;
5851 } else if ( fs_is_network_admin() ) {
5852 /**
5853 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
5854 */
5855 $non_delegated_sites = $this->get_non_delegated_blog_ids();
5856
5857 foreach ( $non_delegated_sites as $blog_id ) {
5858 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5859
5860 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
5861 $this->_is_anonymous = false;
5862 break;
5863 }
5864 }
5865
5866 if ( false !== $this->_is_anonymous ) {
5867 $this->_is_anonymous = true;
5868 }
5869 } else {
5870 if ( ! isset( $this->_storage->is_anonymous ) ) {
5871 // Not skipped.
5872 $this->_is_anonymous = false;
5873 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
5874 // For back compatibility, since the variable was boolean before.
5875 $this->_is_anonymous = $this->_storage->is_anonymous;
5876
5877 // Upgrade stored data format to 1.1.3 format.
5878 $this->set_anonymous_mode( $this->_storage->is_anonymous );
5879 } else {
5880 // Version 1.1.3 and later.
5881 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
5882 }
5883 }
5884 }
5885
5886 return $this->_is_anonymous;
5887 }
5888
5889 /**
5890 * Check if the user skipped the connection of a specified site.
5891 *
5892 * @author Vova Feldman (@svovaf)
5893 * @since 2.0.0
5894 *
5895 * @param int $blog_id
5896 *
5897 * @return bool
5898 */
5899 function is_anonymous_site( $blog_id = 0 ) {
5900 if ( $this->is_network_anonymous() ) {
5901 return true;
5902 }
5903
5904 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5905
5906 if ( empty( $is_anonymous ) ) {
5907 return false;
5908 }
5909
5910 return $is_anonymous['is'];
5911 }
5912
5913 /**
5914 * Check if user connected his account and install pending email activation.
5915 *
5916 * @author Vova Feldman (@svovaf)
5917 * @since 1.0.7
5918 *
5919 * @return bool
5920 */
5921 function is_pending_activation() {
5922 return $this->_storage->get( 'is_pending_activation', false );
5923 }
5924
5925 /**
5926 * @author Leo Fajardo (@leorw)
5927 * @since 2.5.0
5928 */
5929 private function clear_pending_activation_mode() {
5930 // Remove the pending activation sticky notice (if it still exists).
5931 $this->_admin_notices->remove_sticky( 'activation_pending' );
5932
5933 // Clear the plugin's pending activation mode.
5934 unset( $this->_storage->is_pending_activation );
5935 }
5936
5937 /**
5938 * Check if plugin must be WordPress.org compliant.
5939 *
5940 * @since 1.0.7
5941 *
5942 * @return bool
5943 */
5944 function is_org_repo_compliant() {
5945 return $this->_is_org_compliant;
5946 }
5947
5948 #--------------------------------------------------------------------------------
5949 #region WP Cron Common
5950 #--------------------------------------------------------------------------------
5951
5952 /**
5953 * @author Vova Feldman (@svovaf)
5954 * @since 2.0.0
5955 *
5956 * @param string $name Cron name.
5957 *
5958 * @return object
5959 */
5960 private function get_cron_data( $name ) {
5961 $this->_logger->entrance( $name );
5962
5963 /**
5964 * @var object $cron_data
5965 */
5966 return $this->_storage->get( "{$name}_cron", null );
5967 }
5968
5969 /**
5970 * @author Vova Feldman (@svovaf)
5971 * @since 2.0.0
5972 *
5973 * @param string $name Cron name.
5974 */
5975 private function clear_cron_data( $name ) {
5976 $this->_logger->entrance( $name );
5977
5978 $this->_storage->remove( "{$name}_cron" );
5979 }
5980
5981 /**
5982 * @author Vova Feldman (@svovaf)
5983 * @since 2.0.0
5984 *
5985 * @param string $name Cron name.
5986 * @param int $cron_blog_id The cron executing blog ID.
5987 */
5988 private function set_cron_data( $name, $cron_blog_id = 0 ) {
5989 $this->_logger->entrance( $name );
5990
5991 $this->_storage->store( "{$name}_cron", (object) array(
5992 'version' => $this->get_plugin_version(),
5993 'blog_id' => $cron_blog_id,
5994 'sdk_version' => $this->version,
5995 'timestamp' => WP_FS__SCRIPT_START_TIME,
5996 'on' => true,
5997 ) );
5998 }
5999
6000 /**
6001 * Get the cron's executing blog ID.
6002 *
6003 * @author Vova Feldman (@svovaf)
6004 * @since 2.0.0
6005 *
6006 * @param string $name Cron name.
6007 *
6008 * @return int
6009 */
6010 private function get_cron_blog_id( $name ) {
6011 $this->_logger->entrance( $name );
6012
6013 if ( ! is_multisite() ) {
6014 // Not a multisite.
6015 return 0;
6016 }
6017
6018 $cron_data = $this->get_cron_data( $name );
6019
6020 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6021 $cron_data->blog_id :
6022 0;
6023 }
6024
6025 /**
6026 * @author Vova Feldman (@svovaf)
6027 * @since 2.0.0
6028 *
6029 * @param string $name Cron name.
6030 *
6031 * @return bool
6032 */
6033 private function is_cron_on( $name ) {
6034 $this->_logger->entrance( $name );
6035
6036 /**
6037 * @var object $cron_data
6038 */
6039 $cron_data = $this->get_cron_data( $name );
6040
6041 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6042 }
6043
6044 /**
6045 * Unix timestamp for previous cron execution or false if never executed.
6046 *
6047 * @author Vova Feldman (@svovaf)
6048 * @since 2.0.0
6049 *
6050 * @param string $name Cron name.
6051 *
6052 * @return int|false
6053 */
6054 private function cron_last_execution( $name ) {
6055 $this->_logger->entrance( $name );
6056
6057 return $this->_storage->get( "{$name}_timestamp" );
6058 }
6059
6060 /**
6061 * Set cron execution time to now.
6062 *
6063 * @author Vova Feldman (@svovaf)
6064 * @since 2.0.0
6065 *
6066 * @param string $name Cron name.
6067 */
6068 private function set_cron_execution_timestamp( $name ) {
6069 $this->_logger->entrance( $name );
6070
6071 $this->_storage->store( "{$name}_timestamp", time() );
6072 }
6073
6074 /**
6075 * Sets the keepalive time to now.
6076 *
6077 * @author Leo Fajardo (@leorw)
6078 * @since 2.2.3
6079 *
6080 * @param bool|null $use_network_level_storage
6081 */
6082 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6083 $this->_logger->entrance();
6084
6085 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6086 }
6087
6088 /**
6089 * Check if cron was executed in the last $period of seconds.
6090 *
6091 * @author Vova Feldman (@svovaf)
6092 * @since 2.0.0
6093 *
6094 * @param string $name Cron name.
6095 * @param int $period In seconds
6096 *
6097 * @return bool
6098 */
6099 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6100 $this->_logger->entrance( $name );
6101
6102 $last_execution = $this->cron_last_execution( $name );
6103
6104 if ( ! is_numeric( $last_execution ) ) {
6105 return false;
6106 }
6107
6108 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6109 }
6110
6111 /**
6112 * WP Cron is executed on a site level. When running in a multisite network environment
6113 * with the network integration activated, for optimization reasons, we are consolidating
6114 * the installs data sync cron to be executed only from a single site.
6115 *
6116 * @author Vova Feldman (@svovaf)
6117 * @since 2.0.0
6118 *
6119 * @param int $except_blog_id Target any except the excluded blog ID.
6120 *
6121 * @return int
6122 */
6123 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6124 if ( ! is_multisite() ) {
6125 return 0;
6126 }
6127
6128 if ( $this->_is_network_active ) {
6129 $network_install_blog_id = $this->_storage->network_install_blog_id;
6130
6131 if (
6132 is_numeric( $network_install_blog_id ) &&
6133 $except_blog_id != $network_install_blog_id &&
6134 self::is_site_active( $network_install_blog_id )
6135 ) {
6136 // Try to run cron from the main network blog.
6137 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6138
6139 if (
6140 is_object( $install ) &&
6141 $this->is_tracking_allowed( $network_install_blog_id, $install )
6142 ) {
6143 return $network_install_blog_id;
6144 }
6145 }
6146 }
6147
6148 // Get first opted-in blog ID with active tracking.
6149 $installs = $this->get_blog_install_map();
6150 foreach ( $installs as $blog_id => $install ) {
6151 if ( $except_blog_id != $blog_id &&
6152 self::is_site_active( $blog_id ) &&
6153 $this->is_tracking_allowed( $blog_id, $install )
6154 ) {
6155 return $blog_id;
6156 }
6157 }
6158
6159 return 0;
6160 }
6161
6162 /**
6163 * @author Vova Feldman (@svovaf)
6164 * @since 2.0.0
6165 *
6166 * @param string $name Cron name.
6167 * @param string $action_tag Callback action tag.
6168 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6169 */
6170 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6171 $this->_logger->entrance( $name );
6172
6173 if ( ! $this->is_cron_on( $name ) ) {
6174 return;
6175 }
6176
6177 $clear_cron = true;
6178 if ( ! $is_network_clear && $this->_is_network_active ) {
6179 $installs = $this->get_blog_install_map();
6180
6181 foreach ( $installs as $blog_id => $install ) {
6182 /**
6183 * @var FS_Site $install
6184 */
6185 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6186 $clear_cron = false;
6187 break;
6188 }
6189 }
6190 }
6191
6192 if ( ! $clear_cron ) {
6193 return;
6194 }
6195
6196 $cron_blog_id = $this->get_cron_blog_id( $name );
6197
6198 $this->clear_cron_data( $name );
6199
6200 if ( 0 < $cron_blog_id ) {
6201 switch_to_blog( $cron_blog_id );
6202 }
6203
6204 if ( empty( $action_tag ) ) {
6205 $action_tag = $name;
6206 }
6207
6208 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6209
6210 if ( 0 < $cron_blog_id ) {
6211 restore_current_blog();
6212 }
6213 }
6214
6215 /**
6216 * Unix timestamp for next cron execution or false if not scheduled.
6217 *
6218 * @author Vova Feldman (@svovaf)
6219 * @since 2.0.0
6220 *
6221 * @param string $name Cron name.
6222 * @param string $action_tag Callback action tag.
6223 *
6224 * @return int|false
6225 */
6226 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6227 $this->_logger->entrance( $name );
6228
6229 if ( ! $this->is_cron_on( $name ) ) {
6230 return false;
6231 }
6232
6233 $cron_blog_id = $this->get_cron_blog_id( $name );
6234
6235 if ( 0 < $cron_blog_id ) {
6236 switch_to_blog( $cron_blog_id );
6237 }
6238
6239 if ( empty( $action_tag ) ) {
6240 $action_tag = $name;
6241 }
6242
6243 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6244
6245 if ( 0 < $cron_blog_id ) {
6246 restore_current_blog();
6247 }
6248
6249 return $next_scheduled;
6250 }
6251
6252 /**
6253 * @author Vova Feldman (@svovaf)
6254 * @since 2.0.0
6255 *
6256 * @param string $name Cron name.
6257 * @param string $action_tag Callback action tag.
6258 * @param string $recurrence 'single' or 'daily'.
6259 * @param int $start_at Defaults to now.
6260 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6261 * @param int $except_blog_id Target any except the excluded blog ID.
6262 */
6263 private function schedule_cron(
6264 $name,
6265 $action_tag = '',
6266 $recurrence = 'single',
6267 $start_at = WP_FS__SCRIPT_START_TIME,
6268 $randomize_start = true,
6269 $except_blog_id = 0
6270 ) {
6271 $this->_logger->entrance( $name );
6272
6273 $this->clear_cron( $name, $action_tag, true );
6274
6275 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6276
6277 if ( is_multisite() && 0 == $cron_blog_id ) {
6278 // Don't schedule cron since couldn't find a target blog.
6279 return;
6280 }
6281
6282 if ( 0 < $cron_blog_id ) {
6283 switch_to_blog( $cron_blog_id );
6284 }
6285
6286 if ( 'daily' === $recurrence ) {
6287 if ( $randomize_start ) {
6288 // Schedule first sync with a random 12 hour time range from now.
6289 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6290 }
6291
6292 // Schedule daily WP cron.
6293 wp_schedule_event(
6294 $start_at,
6295 'daily',
6296 $this->get_action_tag( $action_tag )
6297 );
6298 } else if ( 'single' === $recurrence ) {
6299 // Schedule single cron.
6300 wp_schedule_single_event(
6301 $start_at,
6302 $this->get_action_tag( $action_tag )
6303 );
6304 }
6305
6306 $this->set_cron_data( $name, $cron_blog_id );
6307
6308 if ( 0 < $cron_blog_id ) {
6309 restore_current_blog();
6310 }
6311 }
6312
6313 /**
6314 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6315 * that doesn't halt page loading.
6316 *
6317 * @author Vova Feldman (@svovaf)
6318 * @since 2.0.0
6319 *
6320 * @param string $name Cron name.
6321 * @param callable $callable The function that should be executed.
6322 */
6323 private function execute_cron( $name, $callable ) {
6324 $this->_logger->entrance( $name );
6325
6326 // Store the last time data sync was executed.
6327 $this->set_cron_execution_timestamp( $name );
6328
6329 // Check if API is temporary down.
6330 if ( FS_Api::is_temporary_down() ) {
6331 return;
6332 }
6333
6334 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6335
6336 $users_2_blog_ids = array();
6337
6338 if ( ! is_multisite() ) {
6339 // Add dummy blog.
6340 $users_2_blog_ids[0] = array( 0 );
6341 } else {
6342 $installs = $this->get_blog_install_map();
6343 foreach ( $installs as $blog_id => $install ) {
6344 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6345 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6346 $users_2_blog_ids[ $install->user_id ] = array();
6347 }
6348
6349 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6350 }
6351 }
6352 }
6353
6354 $current_blog_id = get_current_blog_id();
6355
6356 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6357 if ( 0 < $blog_ids[0] ) {
6358 $this->switch_to_blog( $blog_ids[0] );
6359 }
6360
6361 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6362
6363 foreach ( $blog_ids as $blog_id ) {
6364 $this->do_action( "after_{$name}_cron", $blog_id );
6365 }
6366 }
6367
6368 if ( is_multisite() ) {
6369 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6370
6371 $this->do_action( "after_{$name}_cron_multisite" );
6372 }
6373 }
6374
6375 #endregion
6376
6377 #----------------------------------------------------------------------------------
6378 #region Daily Sync Cron
6379 #----------------------------------------------------------------------------------
6380
6381
6382 /**
6383 * @author Vova Feldman (@svovaf)
6384 * @since 2.0.0
6385 *
6386 * @return bool
6387 */
6388 private function is_sync_cron_scheduled() {
6389 return $this->is_cron_on( 'sync' );
6390 }
6391
6392 /**
6393 * Get the sync cron's executing blog ID.
6394 *
6395 * @author Vova Feldman (@svovaf)
6396 * @since 2.0.0
6397 *
6398 * @return int
6399 */
6400 private function get_sync_cron_blog_id() {
6401 return $this->get_cron_blog_id( 'sync' );
6402 }
6403
6404 /**
6405 * @author Vova Feldman (@svovaf)
6406 * @since 1.1.7.3
6407 */
6408 private function run_manual_sync() {
6409 if ( ! $this->is_user_admin() ) {
6410 return;
6411 }
6412
6413 // Run manual sync.
6414 $this->_sync_cron();
6415
6416 // Reschedule next cron to run 24 hours from now (performance optimization).
6417 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6418 }
6419
6420 /**
6421 * Data sync cron job. Replaces the background sync non blocking HTTP request
6422 * that doesn't halt page loading.
6423 *
6424 * @author Vova Feldman (@svovaf)
6425 * @since 1.1.7.3
6426 * @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.
6427 */
6428 function _sync_cron() {
6429 $this->_logger->entrance();
6430
6431 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6432 }
6433
6434 /**
6435 * The actual data sync cron logic.
6436 *
6437 * @author Vova Feldman (@svovaf)
6438 * @since 2.0.0
6439 *
6440 * @param int[] $blog_ids
6441 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6442 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6443 * updates for a single site in case `execute_cron` has switched to a different blog.
6444 */
6445 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6446 if ( $this->is_registered() ) {
6447 if ( $this->has_paid_plan() ) {
6448 // Initiate background plan sync.
6449 $this->_sync_license( true, false, $current_blog_id );
6450
6451 if ( $this->is_paying() ) {
6452 // Check for premium plugin updates.
6453 $this->check_updates( true );
6454 }
6455 } else {
6456 // Sync install(s) (only if something changed locally).
6457 if ( 1 < count( $blog_ids ) ) {
6458 $this->sync_installs();
6459 } else {
6460 $this->sync_install();
6461 }
6462
6463 $this->maybe_sync_install_user();
6464 }
6465 }
6466 }
6467
6468 /**
6469 * Check if sync was executed in the last $period of seconds.
6470 *
6471 * @author Vova Feldman (@svovaf)
6472 * @since 1.1.7.3
6473 *
6474 * @param int $period In seconds
6475 *
6476 * @return bool
6477 */
6478 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6479 return $this->is_cron_executed( 'sync', $period );
6480 }
6481
6482 /**
6483 * @author Vova Feldman (@svovaf)
6484 * @since 1.1.7.3
6485 *
6486 * @return bool
6487 */
6488 private function is_sync_cron_on() {
6489 return $this->is_cron_on( 'sync' );
6490 }
6491
6492 /**
6493 * @author Leo Fajardo (@leorw)
6494 * @since 2.5.0
6495 */
6496 private function maybe_schedule_sync_cron() {
6497 $next_schedule = $this->next_sync_cron();
6498
6499 // The event is properly scheduled, so no need to reschedule it.
6500 if (
6501 is_numeric( $next_schedule ) &&
6502 $next_schedule > time()
6503 ) {
6504 return;
6505 }
6506
6507 $this->schedule_sync_cron();
6508 }
6509
6510 /**
6511 * @author Vova Feldman (@svovaf)
6512 * @since 1.1.7.3
6513 *
6514 * @param int $start_at Defaults to now.
6515 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6516 * @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.
6517 */
6518 private function schedule_sync_cron(
6519 $start_at = WP_FS__SCRIPT_START_TIME,
6520 $randomize_start = true,
6521 $except_blog_id = 0
6522 ) {
6523 $this->schedule_cron(
6524 'sync',
6525 'data_sync',
6526 'daily',
6527 $start_at,
6528 $randomize_start,
6529 $except_blog_id
6530 );
6531 }
6532
6533 /**
6534 * Add the actual sync function to the cron job hook.
6535 *
6536 * @author Vova Feldman (@svovaf)
6537 * @since 1.1.7.3
6538 */
6539 private function hook_callback_to_sync_cron() {
6540 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6541 }
6542
6543 /**
6544 * @author Vova Feldman (@svovaf)
6545 * @since 1.1.7.3
6546 *
6547 * @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.
6548 */
6549 private function clear_sync_cron( $is_network_clear = false ) {
6550 $this->_logger->entrance();
6551
6552 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6553 }
6554
6555 /**
6556 * Unix timestamp for next sync cron execution or false if not scheduled.
6557 *
6558 * @author Vova Feldman (@svovaf)
6559 * @since 1.1.7.3
6560 *
6561 * @return int|false
6562 */
6563 function next_sync_cron() {
6564 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6565 }
6566
6567 /**
6568 * Unix timestamp for previous sync cron execution or false if never executed.
6569 *
6570 * @author Vova Feldman (@svovaf)
6571 * @since 1.1.7.3
6572 *
6573 * @return int|false
6574 */
6575 function last_sync_cron() {
6576 return $this->cron_last_execution( 'sync' );
6577 }
6578
6579 #endregion Daily Sync Cron ------------------------------------------------------------------
6580
6581 #----------------------------------------------------------------------------------
6582 #region Async Install Sync
6583 #----------------------------------------------------------------------------------
6584
6585 /**
6586 * @author Vova Feldman (@svovaf)
6587 * @since 1.1.7.3
6588 *
6589 * @return bool
6590 */
6591 private function is_install_sync_scheduled() {
6592 return $this->is_cron_on( 'install_sync' );
6593 }
6594
6595 /**
6596 * Get the sync cron's executing blog ID.
6597 *
6598 * @author Vova Feldman (@svovaf)
6599 * @since 2.0.0
6600 *
6601 * @return int
6602 */
6603 private function get_install_sync_cron_blog_id() {
6604 return $this->get_cron_blog_id( 'install_sync' );
6605 }
6606
6607 /**
6608 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6609 *
6610 * @author Vova Feldman (@svovaf)
6611 * @since 1.1.7.3
6612 *
6613 * @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.
6614 */
6615 private function schedule_install_sync( $except_blog_id = 0 ) {
6616 if ( $this->is_clone() ) {
6617 return;
6618 }
6619
6620 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6621 }
6622
6623 /**
6624 * Unix timestamp for previous install sync cron execution or false if never executed.
6625 *
6626 * @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.
6627 *
6628 * @author Vova Feldman (@svovaf)
6629 * @since 1.1.7.3
6630 *
6631 * @return int|false
6632 */
6633 function last_install_sync() {
6634 return $this->cron_last_execution( 'install_sync' );
6635 }
6636
6637 /**
6638 * Unix timestamp for next install sync cron execution or false if not scheduled.
6639 *
6640 * @author Vova Feldman (@svovaf)
6641 * @since 1.1.7.3
6642 *
6643 * @return int|false
6644 */
6645 function next_install_sync() {
6646 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6647 }
6648
6649 /**
6650 * Add the actual install sync function to the cron job hook.
6651 *
6652 * @author Vova Feldman (@svovaf)
6653 * @since 1.1.7.3
6654 */
6655 private function hook_callback_to_install_sync() {
6656 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6657 }
6658
6659 /**
6660 * @author Vova Feldman (@svovaf)
6661 * @since 1.1.7.3
6662 *
6663 * @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.
6664 */
6665 private function clear_install_sync_cron( $is_network_clear = false ) {
6666 $this->_logger->entrance();
6667
6668 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6669 }
6670
6671 /**
6672 * @author Vova Feldman (@svovaf)
6673 * @since 1.1.7.3
6674 * @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.
6675 */
6676 public function _run_sync_install() {
6677 $this->_logger->entrance();
6678
6679 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6680 }
6681
6682 /**
6683 * The actual install(s) sync cron logic.
6684 *
6685 * @author Vova Feldman (@svovaf)
6686 * @since 2.0.0
6687 *
6688 * @param int[] $blog_ids
6689 * @param int|null $current_blog_id
6690 */
6691 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6692 if ( $this->is_registered() ) {
6693 if ( 1 < count( $blog_ids ) ) {
6694 $this->sync_installs( array(), true );
6695 } else {
6696 $this->sync_install( array(), true );
6697 }
6698
6699 $this->maybe_sync_install_user();
6700 }
6701 }
6702
6703 #endregion Async Install Sync ------------------------------------------------------------------
6704
6705 /**
6706 * Show a notice that activation is currently pending.
6707 *
6708 * @todo Add some sort of mechanism to allow users to update the email address they would like to opt-in with when $is_suspicious_email is true.
6709 *
6710 * @author Vova Feldman (@svovaf)
6711 * @since 1.0.7
6712 *
6713 * @param bool|string $email_address
6714 * @param bool $is_pending_trial Since 1.2.1.5
6715 * @param bool $is_suspicious_email Since 2.5.0 Set to true when there's an indication that email address the user opted in with is fake/dummy/placeholder.
6716 * @param bool $has_upgrade_context Since 2.5.3
6717 * @param bool $support_email_address Since 2.5.3
6718 */
6719 function _add_pending_activation_notice(
6720 $email_address = false,
6721 $is_pending_trial = false,
6722 $is_suspicious_email = false,
6723 $has_upgrade_context = false,
6724 $support_email_address = false
6725 ) {
6726 if ( ! is_string( $email_address ) ) {
6727 $current_user = self::_get_current_wp_user();
6728 $email_address = $current_user->user_email;
6729 }
6730
6731 $formatted_message_args = array(
6732 "<b>{$this->get_plugin_name()}</b>",
6733 "<b>{$email_address}</b>",
6734 );
6735
6736 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
6737 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
6738 $formatted_message = $this->get_text_inline( 'You should receive a confirmation email for %1$s to your mailbox at %2$s. Please make sure you click the button in that email to %3$s.', 'pending-activation-message' );
6739
6740 $formatted_message_args[] = $is_pending_trial ?
6741 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6742 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
6743
6744 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
6745 } else {
6746 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
6747 $formatted_message = $this->get_text_inline( 'You should receive %3$s for %1$s to your mailbox at %2$s in the next 5 minutes.' );
6748
6749 if ( $this->has_release_on_freemius() ) {
6750 $formatted_message_args[] = $this->get_text_x_inline(
6751 'the installation instructions',
6752 'Part of the message telling the user what they should receive via email.',
6753 'the-installation-instructions-phrase'
6754 );
6755 } else {
6756 $formatted_message_args[] = $this->get_text_x_inline(
6757 'a license key',
6758 'Part of the message telling the user what they should receive via email.',
6759 'a-license-key-phrase'
6760 );
6761
6762 $formatted_message .= ( ' ' . sprintf(
6763 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
6764 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
6765 sprintf(
6766 '<b><a href="%s">%s</a></b>',
6767 $this->get_activation_url( array(
6768 'fs_action' => 'reset_pending_activation_mode',
6769 'require_license' => 'true',
6770 'fs_unique_affix' => $this->get_unique_affix(),
6771 ) ),
6772 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
6773 )
6774 ) );
6775 }
6776
6777 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
6778 ( "<b>{$support_email_address}</b>" ) :
6779 $this->get_text_x_inline(
6780 "the product's support email address",
6781 'Part of the message that tells the user to check their spam folder for a specific email.',
6782 'product-support-email-address-phrase'
6783 );
6784
6785 $formatted_message .= ( ' ' . $this->get_text_inline( 'If you didn\'t get the email, try checking your spam folder or search for emails from %4$s.', 'check-spam-folder-message' ) );
6786
6787 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
6788 }
6789
6790 $this->_admin_notices->add_sticky(
6791 vsprintf( $formatted_message, $formatted_message_args ),
6792 'activation_pending',
6793 $notice_title
6794 );
6795 }
6796
6797 /**
6798 * Check if currently in plugin activation.
6799 *
6800 * @author Vova Feldman (@svovaf)
6801 * @since 1.1.4
6802 *
6803 * @return bool
6804 */
6805 function is_plugin_activation() {
6806 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6807
6808 return !empty($result);
6809 }
6810
6811 /**
6812 *
6813 * NOTE: admin_menu action executed before admin_init.
6814 *
6815 * @author Vova Feldman (@svovaf)
6816 * @since 1.0.7
6817 */
6818 function _admin_init_action() {
6819 $is_migration = $this->is_migration();
6820
6821 /**
6822 * Automatically redirect to connect/activation page after plugin activation.
6823 *
6824 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6825 */
6826 if ( $this->is_plugin_activation() ) {
6827 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6828
6829 if ( isset( $_GET['activate-multi'] ) ) {
6830 /**
6831 * Don't redirect if activating multiple plugins at once (bulk activation).
6832 */
6833 } else if (
6834 self::is_deactivation_snoozed() &&
6835 (
6836 // Either running the free code base.
6837 ! $this->is_premium() ||
6838 // Or if has a free version.
6839 ! $this->is_only_premium() ||
6840 // If premium only, don't redirect if license is activated.
6841 ( $this->is_registered() && ! $this->can_use_premium_code() )
6842 )
6843 ) {
6844 /**
6845 * Don't redirect if activating during the deactivation snooze period (aka troubleshooting), unless activating a paid product version that the admin didn't enter its license key yet.
6846 */
6847 } else if ( ! $is_migration ) {
6848 $this->_redirect_on_activation_hook();
6849 return;
6850 }
6851 }
6852
6853 if ( $is_migration ) {
6854 return;
6855 }
6856
6857 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6858 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6859
6860 $this->skip_connection( fs_is_network_admin() );
6861
6862 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6863 }
6864
6865 if ( $this->is_network_activation_mode() &&
6866 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6867 ) {
6868 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6869
6870 $this->delegate_connection();
6871
6872 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6873 }
6874
6875 $this->_add_upgrade_action_link();
6876
6877 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
6878 (
6879 ( true === $this->_storage->require_license_activation ) ||
6880 // Not registered nor anonymous.
6881 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6882 // OR, network level and in network upgrade mode.
6883 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6884 )
6885 ) {
6886 if ( ! $this->is_pending_activation() ) {
6887 if ( ! $this->is_activation_page() ) {
6888 /**
6889 * If a user visits any other admin page before activating the premium-only theme with a valid
6890 * license, reactivate the previous theme.
6891 *
6892 * @author Leo Fajardo (@leorw)
6893 * @since 1.2.2
6894 */
6895 if ( $this->is_theme() &&
6896 ! $this->has_settings_menu() &&
6897 ! isset( $_REQUEST['fs_action'] ) &&
6898 $this->can_activate_previous_theme()
6899 ) {
6900 if ( $this->is_only_premium() ) {
6901 $this->activate_previous_theme();
6902 return;
6903 }
6904
6905 if ( true === $this->_storage->require_license_activation ) {
6906 $this->_storage->require_license_activation = false;
6907 }
6908 }
6909
6910 if ( ! fs_is_network_admin() &&
6911 $this->is_network_activation_mode() &&
6912 ! $this->is_delegated_connection()
6913 ) {
6914 return;
6915 }
6916
6917 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6918 if ( ! $this->_anonymous_mode &&
6919 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
6920 // Show notice for new plugin installations.
6921 $this->_admin_notices->add(
6922 sprintf(
6923 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6924 sprintf( '<b><a href="%s">%s</a></b>',
6925 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6926 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
6927 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
6928 )
6929 ),
6930 '',
6931 'update-nag'
6932 );
6933 }
6934 } else {
6935
6936 if ( $this->has_filter( 'optin_pointer_element' ) ) {
6937 // Don't show admin nag if plugin update.
6938 wp_enqueue_script( 'wp-pointer' );
6939 wp_enqueue_style( 'wp-pointer' );
6940
6941 $this->_enqueue_connect_essentials();
6942
6943 add_action( 'admin_print_footer_scripts', array(
6944 $this,
6945 '_add_connect_pointer_script'
6946 ) );
6947 }
6948 }
6949 }
6950 }
6951
6952 if ( $this->show_opt_in_on_themes_page() &&
6953 $this->is_activation_page()
6954 ) {
6955 $this->_show_theme_activation_optin_dialog();
6956 }
6957 }
6958 }
6959
6960 /**
6961 * Enqueue connect requires scripts and styles.
6962 *
6963 * @author Vova Feldman (@svovaf)
6964 * @since 1.1.4
6965 */
6966 function _enqueue_connect_essentials() {
6967 wp_enqueue_script( 'jquery' );
6968 wp_enqueue_script( 'json2' );
6969
6970 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
6971 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
6972 }
6973
6974 /**
6975 * Add connect / opt-in pointer.
6976 *
6977 * @author Vova Feldman (@svovaf)
6978 * @since 1.1.4
6979 */
6980 function _add_connect_pointer_script() {
6981 $vars = array( 'id' => $this->_module_id );
6982 $pointer_content = fs_get_template( 'connect.php', $vars );
6983 ?>
6984 <script type="text/javascript">// <![CDATA[
6985 jQuery(document).ready(function ($) {
6986 if ('undefined' !== typeof(jQuery().pointer)) {
6987
6988 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
6989
6990 if (element.length > 0) {
6991 var optin = $(element).pointer($.extend(true, {}, {
6992 content : <?php echo json_encode( $pointer_content ) ?>,
6993 position : {
6994 edge : 'left',
6995 align: 'center'
6996 },
6997 buttons : function () {
6998 // Don't show pointer buttons.
6999 return '';
7000 },
7001 pointerWidth: 482
7002 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7003
7004 <?php
7005 echo $this->apply_filters( 'optin_pointer_execute', "
7006
7007 optin.pointer('open');
7008
7009 // Tag the opt-in pointer with custom class.
7010 $('.wp-pointer #fs_connect')
7011 .parents('.wp-pointer.wp-pointer-top')
7012 .addClass('fs-opt-in-pointer');
7013
7014 ", 'element', 'optin' ) ?>
7015 }
7016 }
7017 });
7018 // ]]></script>
7019 <?php
7020 }
7021
7022 /**
7023 * Return current page's URL.
7024 *
7025 * @author Vova Feldman (@svovaf)
7026 * @since 1.0.7
7027 *
7028 * @return string
7029 */
7030 static function current_page_url() {
7031 $url = 'http';
7032
7033 if ( isset( $_SERVER["HTTPS"] ) ) {
7034 if ( $_SERVER["HTTPS"] == "on" ) {
7035 $url .= "s";
7036 }
7037 }
7038 $url .= "://";
7039 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7040 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7041 } else {
7042 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7043 }
7044
7045 return esc_url( $url );
7046 }
7047
7048 /**
7049 * Check if the current page is the plugin's main admin settings page.
7050 *
7051 * @author Vova Feldman (@svovaf)
7052 * @since 1.0.7
7053 *
7054 * @return bool
7055 */
7056 function _is_plugin_page() {
7057 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7058 fs_is_plugin_page( $this->_slug );
7059 }
7060
7061 /* Events
7062 ------------------------------------------------------------------------------------------------------------------*/
7063 /**
7064 * Delete site install from Database.
7065 *
7066 * @author Vova Feldman (@svovaf)
7067 * @since 1.0.1
7068 *
7069 * @param bool $store
7070 * @param int|null $blog_id Since 2.0.0
7071 *
7072 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7073 */
7074 function _delete_site( $store = true, $blog_id = null ) {
7075 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7076 }
7077
7078 /**
7079 * Delete site install from Database.
7080 *
7081 * @author Vova Feldman (@svovaf)
7082 * @since 1.2.2.7
7083 *
7084 * @param string $slug
7085 * @param string $module_type
7086 * @param bool $store
7087 * @param int|null $blog_id Since 2.0.0
7088 *
7089 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7090 */
7091 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7092 $sites = self::get_all_sites( $module_type, $blog_id );
7093
7094 $install_id = false;
7095
7096 if ( isset( $sites[ $slug ] ) ) {
7097 if ( is_object( $sites[ $slug ] ) ) {
7098 $install_id = $sites[ $slug ]->id;
7099 }
7100
7101 unset( $sites[ $slug ] );
7102
7103 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7104 }
7105
7106 return $install_id;
7107 }
7108
7109 /**
7110 * Delete plugin's plans information.
7111 *
7112 * @param bool $store Flush to Database if true.
7113 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7114 *
7115 * @author Vova Feldman (@svovaf)
7116 * @since 1.0.9
7117 */
7118 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7119 $this->_logger->entrance();
7120
7121 $plans = self::get_all_plans( $this->_module_type );
7122
7123 $plans_to_keep = array();
7124
7125 if ( $keep_associated_plans ) {
7126 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7127 foreach ( $plans_ids_to_keep as $plan_id ) {
7128 $plan = self::_get_plan_by_id( $plan_id );
7129 if ( is_object( $plan ) ) {
7130 $plans_to_keep[] = self::_encrypt_entity( $plan );
7131 }
7132 }
7133 }
7134
7135 if ( ! empty( $plans_to_keep ) ) {
7136 $plans[ $this->_slug ] = $plans_to_keep;
7137 } else {
7138 unset( $plans[ $this->_slug ] );
7139 }
7140
7141 $this->set_account_option( 'plans', $plans, $store );
7142 }
7143
7144 /**
7145 * Delete all plugin licenses.
7146 *
7147 * @author Vova Feldman (@svovaf)
7148 * @since 1.0.9
7149 *
7150 * @param bool $store
7151 */
7152 private function _delete_licenses( $store = true ) {
7153 $this->_logger->entrance();
7154
7155 $all_licenses = self::get_all_licenses();
7156
7157 unset( $all_licenses[ $this->_module_id ] );
7158
7159 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7160 }
7161
7162 /**
7163 * Check if Freemius was added on new plugin installation.
7164 *
7165 * @author Vova Feldman (@svovaf)
7166 * @since 1.1.5
7167 *
7168 * @return bool
7169 */
7170 function is_plugin_new_install() {
7171 return isset( $this->_storage->is_plugin_new_install ) &&
7172 $this->_storage->is_plugin_new_install;
7173 }
7174
7175 /**
7176 * Check if it's the first plugin release that is running Freemius.
7177 *
7178 * @author Vova Feldman (@svovaf)
7179 * @since 1.2.1.5
7180 *
7181 * @return bool
7182 */
7183 function is_first_freemius_powered_version() {
7184 return empty( $this->_storage->plugin_last_version );
7185 }
7186
7187 /**
7188 * @author Leo Fajardo (@leorw)
7189 * @since 1.2.2
7190 *
7191 * @return bool|string
7192 */
7193 private function get_previous_theme_slug() {
7194 return isset( $this->_storage->previous_theme ) ?
7195 $this->_storage->previous_theme :
7196 false;
7197 }
7198
7199 /**
7200 * @author Leo Fajardo (@leorw)
7201 * @since 1.2.2
7202 *
7203 * @return bool
7204 */
7205 private function can_activate_previous_theme() {
7206 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7207 }
7208
7209 /**
7210 * @author Leo Fajardo (@leorw)
7211 * @since 2.5.0
7212 *
7213 * @return bool
7214 */
7215 private function can_activate_theme( $slug ) {
7216 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7217 $theme_instance = wp_get_theme( $slug );
7218
7219 return $theme_instance->exists();
7220 }
7221
7222 return false;
7223 }
7224
7225 /**
7226 * @author Leo Fajardo (@leorw)
7227 * @since 1.2.2
7228 */
7229 private function activate_previous_theme() {
7230 switch_theme( $this->get_previous_theme_slug() );
7231 unset( $this->_storage->previous_theme );
7232
7233 global $pagenow;
7234 if ( 'themes.php' === $pagenow ) {
7235 /**
7236 * Refresh the active theme information.
7237 *
7238 * @author Leo Fajardo (@leorw)
7239 * @since 1.2.2
7240 */
7241 fs_redirect( $this->admin_url( $pagenow ) );
7242 }
7243 }
7244
7245 /**
7246 * @author Leo Fajardo (@leorw)
7247 * @since 1.2.2
7248 *
7249 * @return string
7250 */
7251 function get_previous_theme_activation_url() {
7252 if ( ! $this->can_activate_previous_theme() ) {
7253 return '';
7254 }
7255
7256 /**
7257 * Activation URL
7258 *
7259 * @author Leo Fajardo (@leorw)
7260 * @since 1.2.2
7261 */
7262 return wp_nonce_url(
7263 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7264 'switch-theme_' . $this->get_previous_theme_slug()
7265 );
7266 }
7267
7268 /**
7269 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7270 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7271 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7272 * theme doesn't exist, then there will be no close button.
7273 *
7274 * @author Leo Fajardo (@leorw)
7275 * @since 1.2.2
7276 *
7277 * @param string $slug_or_name Old theme's slug or name.
7278 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7279 */
7280 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7281 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7282 $old_theme->get_stylesheet() :
7283 $slug_or_name;
7284
7285 $this->_activate_plugin_event_hook();
7286 }
7287
7288 /**
7289 * Plugin activated hook.
7290 *
7291 * @author Vova Feldman (@svovaf)
7292 * @since 1.0.1
7293 *
7294 * @uses FS_Api
7295 */
7296 function _activate_plugin_event_hook() {
7297 $this->_logger->entrance( 'slug = ' . $this->_slug );
7298
7299 if ( ! $this->is_user_admin() ) {
7300 return;
7301 }
7302
7303 $this->unregister_uninstall_hook();
7304
7305 // Clear API cache on activation.
7306 FS_Api::clear_cache();
7307
7308 $is_premium_version_activation = $this->is_plugin() ?
7309 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7310 $this->is_premium();
7311
7312 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7313 $this->clear_pending_activation_mode();
7314 }
7315
7316 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7317
7318 if ( $this->is_plugin() ) {
7319 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7320 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7321 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7322 $other_version_basename = $is_premium_version_activation ?
7323 $this->_free_plugin_basename :
7324 $this->premium_plugin_basename();
7325
7326 if ( ! $this->_is_network_active ) {
7327 /**
7328 * Themes are always network activated, but the ACTUAL activation is per site.
7329 *
7330 * During the activation, the plugin isn't yet active, therefore,
7331 * _is_network_active will be set to false even if it's a network level
7332 * activation. So we need to fix that by looking at the is_network_admin() value.
7333 *
7334 * @author Vova Feldman
7335 */
7336 $this->_is_network_active = (
7337 $this->_is_multisite_integrated &&
7338 fs_is_network_admin()
7339 );
7340 }
7341
7342 /**
7343 * If the other module version is active, deactivate it.
7344 *
7345 * is_plugin_active() checks if the plugin is active on the site or the network level and
7346 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7347 *
7348 * @author Leo Fajardo (@leorw)
7349 * @since 1.2.2
7350 */
7351 if (
7352 is_plugin_active( $other_version_basename ) &&
7353 $this->apply_filters( 'deactivate_on_activation', true )
7354 ) {
7355 deactivate_plugins( $other_version_basename );
7356 }
7357 }
7358
7359 if ( $this->is_registered() ) {
7360 if ( $is_premium_version_activation ) {
7361 $this->reconnect_locally();
7362 }
7363
7364
7365 // Schedule re-activation event and sync.
7366 // $this->sync_install( array(), true );
7367 $this->schedule_install_sync();
7368
7369 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7370 if ( $is_premium_version_activation ) {
7371 $this->_admin_notices->add(
7372 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7373 $this->get_text_x_inline( 'W00t',
7374 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7375 );
7376 }
7377 } else if ( $this->is_anonymous() ) {
7378 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7379 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7380 $network = true;
7381 } else {
7382 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7383 $this->_storage->is_anonymous['version'] :
7384 null;
7385 $network = false;
7386 }
7387
7388 /**
7389 * Reset "skipped" click cache on the following:
7390 * 1. Freemius DEV mode.
7391 * 2. WordPress DEBUG mode.
7392 * 3. If a plugin and the user skipped the exact same version before.
7393 *
7394 * @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).
7395 *
7396 * @todo 4. If explicitly asked to retry after every activation.
7397 */
7398 if ( WP_FS__DEV_MODE ||
7399 (
7400 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7401 $this->get_plugin_version() == $plugin_version
7402 )
7403 ) {
7404 $this->reset_anonymous_mode( $network );
7405 }
7406 }
7407
7408 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7409
7410 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7411 /**
7412 * When activating an add-on, try to also activate a license.
7413 *
7414 * @author Leo Fajardo (@leorw)
7415 * @since 2.3.0
7416 */
7417 if ( ! $this->_is_network_active ) {
7418 $this->maybe_activate_addon_license();
7419 } else {
7420 $this->maybe_network_activate_addon_license();
7421 }
7422
7423 /**
7424 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7425 *
7426 * @author Leo Fajardo (@leorw)
7427 * @since 2.3.0
7428 */
7429 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7430
7431 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7432 $this->_storage->require_license_activation = false;
7433 }
7434 }
7435
7436 if (
7437 $is_premium_version_activation &&
7438 (
7439 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7440 (
7441 $this->is_registered() &&
7442 ! $is_trial_or_has_features_enabled_license
7443 )
7444 )
7445 ) {
7446 $this->_storage->require_license_activation = true;
7447 }
7448
7449 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7450 /**
7451 * If no previous version of plugin's version exist, it means that it's either
7452 * the first time that the plugin installed on the site, or the plugin was installed
7453 * before but didn't have Freemius integrated.
7454 *
7455 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7456 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7457 * only after immediate activation.
7458 *
7459 * @since 1.1.4
7460 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7461 */
7462 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7463 }
7464
7465 /**
7466 * Also flush when activating the premium version so that even if Freemius was off before, the API
7467 * connectivity test can be run again.
7468 *
7469 * @author Leo Fajardo (@leorw)
7470 * @since 2.2.3.1
7471 */
7472 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7473
7474 if ( ! $this->_anonymous_mode &&
7475 ( false !== $has_api_connectivity ) &&
7476 ! $this->_isAutoInstall
7477 ) {
7478 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7479 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7480 }
7481
7482 /**
7483 * Activation hook is executed after the plugin's main file is loaded, therefore,
7484 * after the plugin was loaded. The logic is located at activate_plugin()
7485 * ./wp-admin/includes/plugin.php.
7486 *
7487 * @author Vova Feldman (@svovaf)
7488 * @since 1.1.9
7489 */
7490 $this->_storage->was_plugin_loaded = true;
7491 }
7492
7493 /**
7494 * @author Leo Fajardo (@leorw)
7495 * @since 2.3.0
7496 */
7497 private function maybe_activate_addon_license() {
7498 $parent_fs = $this->get_parent_instance();
7499
7500 if (
7501 ! is_object( $parent_fs ) ||
7502 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7503 ) {
7504 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7505 return;
7506 }
7507
7508 $license = $this->get_active_parent_license();
7509 if ( ! is_object( $license ) ) {
7510 return;
7511 }
7512
7513 if (
7514 $this->is_bundle_license_auto_activation_enabled() &&
7515 ! empty( $license->products )
7516 ) {
7517 $this->activate_bundle_license( $license );
7518
7519 return;
7520 }
7521
7522 if ( ! $this->is_registered() ) {
7523 // Opt in with a license key.
7524 $this->opt_in(
7525 $parent_fs->get_current_or_network_user()->email,
7526 false,
7527 false,
7528 $license->secret_key
7529 );
7530 } else {
7531 // Activate the license.
7532 $install = $this->api_site_call(
7533 '/',
7534 'put',
7535 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7536 );
7537
7538 if ( ! FS_Api::is_api_error( $install ) ) {
7539 $this->_sync_addon_license( $this->get_id(), true );
7540 }
7541 }
7542 }
7543
7544 /**
7545 * @author Leo Fajardo (@leorw)
7546 * @since 2.3.0
7547 *
7548 * @param FS_Plugin_License $license
7549 */
7550 private function maybe_network_activate_addon_license( $license = null ) {
7551 $parent_fs = $this->get_parent_instance();
7552 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7553 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7554 return;
7555 }
7556
7557 $license = ( ! is_null( $license ) ) ?
7558 $license :
7559 $this->get_active_parent_license();
7560
7561 if ( ! is_object( $license ) ) {
7562 return;
7563 }
7564
7565 if (
7566 $this->is_bundle_license_auto_activation_enabled() &&
7567 ! empty( $license->products )
7568 ) {
7569 $this->activate_bundle_license( $license );
7570
7571 return;
7572 }
7573
7574 if ( ! $this->is_network_registered() ) {
7575 $sites = $this->get_sites_for_network_level_optin();
7576
7577 if ( count( $sites ) > $license->left() ) {
7578 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7579 return;
7580 }
7581
7582 // Opt in with a license key.
7583 $this->opt_in(
7584 $parent_fs->get_user()->email,
7585 false,
7586 false,
7587 $license->secret_key,
7588 false,
7589 false,
7590 false,
7591 null,
7592 $sites
7593 );
7594 } else {
7595 $blog_2_install_map = array();
7596 $site_ids = array();
7597
7598 $all_sites = Freemius::get_sites();
7599
7600 foreach ( $all_sites as $site ) {
7601 $blog_id = Freemius::get_site_blog_id( $site );
7602 $install = $this->get_install_by_blog_id( $blog_id );
7603
7604 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7605 // Skip license activation for installs that are already associated with a license.
7606 continue;
7607 }
7608
7609 if ( is_object( $install ) ) {
7610 $blog_2_install_map[ $blog_id ] = $install;
7611 } else {
7612 $site_ids[] = $blog_id;
7613 }
7614 }
7615
7616 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7617 return;
7618 }
7619
7620 $user = $this->get_current_or_network_user();
7621
7622 if ( ! empty( $blog_2_install_map ) ) {
7623 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7624
7625 if ( true !== $result ) {
7626 return;
7627 }
7628 }
7629
7630 if ( ! empty( $site_ids ) ) {
7631 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7632 }
7633 }
7634 }
7635
7636 /**
7637 * 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.
7638 *
7639 * @author Leo Fajardo (@leorw)
7640 * @since 2.4.0
7641 *
7642 * @param FS_Plugin_License $license
7643 * @param array $sites
7644 * @param int $blog_id
7645 */
7646 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
7647 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
7648 $license = $this->_license;
7649 }
7650
7651 if ( ! is_object( $license ) ) {
7652 return;
7653 }
7654
7655 $parent_license = ( ! empty( $license->products ) ) ?
7656 $license :
7657 $this->get_active_parent_license( $license->secret_key );
7658
7659 if ( is_object( $parent_license ) ) {
7660 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
7661 }
7662 }
7663
7664 /**
7665 * Try to activate a bundle license for all the bundle products installed on the site.
7666 * (1) If a child product install already has a license, the bundle license won't be activated.
7667 * (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.
7668 * (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.
7669 *
7670 * @author Leo Fajardo (@leorw)
7671 * @since 2.4.0
7672 *
7673 * @param FS_Plugin_License $license
7674 * @param array $sites
7675 * @param int $current_blog_id
7676 */
7677 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
7678 $is_network_admin = fs_is_network_admin();
7679
7680 $installs_by_blog_map = array();
7681 $site_info_by_blog_map = array();
7682
7683 /**
7684 * Try to activate the license for all supported products.
7685 *
7686 * @author Leo Fajardo
7687 */
7688 foreach ( $license->products as $product_id ) {
7689 $fs = self::get_instance_by_id( $product_id );
7690
7691 if ( ! is_object( $fs ) ) {
7692 continue;
7693 }
7694
7695 if ( ! $fs->has_paid_plan() ) {
7696 continue;
7697 }
7698
7699 if (
7700 ! $fs->is_addon() &&
7701 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
7702 ) {
7703 /**
7704 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
7705 * there is a context bundle.
7706 */
7707 continue;
7708 }
7709
7710 if ( $current_blog_id > 0 ) {
7711 $fs->switch_to_blog( $current_blog_id );
7712 }
7713
7714 if ( $fs->has_active_valid_license() ) {
7715 continue;
7716 }
7717
7718 if ( ! $is_network_admin || $current_blog_id > 0 ) {
7719 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
7720 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
7721 continue;
7722 }
7723 } else {
7724 if ( ! $fs->is_network_active() ) {
7725 // Do not try to activate the license in the network level if the product is not network active.
7726 continue;
7727 }
7728
7729 if ( $fs->is_network_delegated_connection() ) {
7730 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
7731 continue;
7732 }
7733
7734 $has_install_with_license = false;
7735
7736 // 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.
7737 $filtered_sites = array();
7738
7739 if ( empty( $sites ) ) {
7740 $all_sites = self::get_sites();
7741
7742 foreach ( $all_sites as $site ) {
7743 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
7744 }
7745 } else {
7746 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
7747 foreach ( $sites as $site ) {
7748 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7749 continue;
7750 }
7751
7752 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
7753 }
7754 }
7755
7756 foreach ( $sites as $site ) {
7757 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7758 continue;
7759 }
7760
7761 $blog_id = $site['blog_id'];
7762
7763 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
7764 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
7765 }
7766
7767 $installs = $installs_by_blog_map[ $blog_id ];
7768 $install = null;
7769
7770 if ( isset( $installs[ $fs->get_slug() ] ) ) {
7771 $install = $installs[ $fs->get_slug() ];
7772
7773 if (
7774 is_object( $install ) &&
7775 (
7776 ! FS_Site::is_valid_id( $install->id ) ||
7777 ! FS_User::is_valid_id( $install->user_id ) ||
7778 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
7779 )
7780 ) {
7781 $install = null;
7782 }
7783 }
7784
7785 if (
7786 is_object( $install ) &&
7787 FS_Plugin_License::is_valid_id( $install->license_id )
7788 ) {
7789 $has_install_with_license = true;
7790 break;
7791 }
7792
7793 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
7794 // Site activation delegated, don't activate bundle license on the site in the network admin.
7795 continue;
7796 }
7797
7798 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
7799 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
7800 }
7801
7802 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
7803 }
7804
7805 if ( $has_install_with_license || empty( $filtered_sites ) ) {
7806 // 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.
7807 continue;
7808 }
7809
7810 $sites = $filtered_sites;
7811 }
7812
7813 $fs->activate_migrated_license(
7814 $license->secret_key,
7815 null,
7816 null,
7817 $sites,
7818 ( $current_blog_id > 0 ? $current_blog_id : null )
7819 );
7820 }
7821 }
7822
7823 /**
7824 * Returns a parent license that can be activated for the context product.
7825 *
7826 * @author Leo Fajardo (@leorw)
7827 * @since 2.3.0
7828 *
7829 * @param string|null $license_key
7830 * @param bool $flush
7831 *
7832 * @return FS_Plugin_License
7833 */
7834 function get_active_parent_license( $license_key = null, $flush = true ) {
7835 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
7836
7837 $fs = $this;
7838
7839 if ( $this->is_addon() ) {
7840 $parent_instance = $this->get_parent_instance();
7841
7842 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
7843 $fs = $parent_instance;
7844 }
7845 }
7846
7847 $foreign_licenses = $fs->get_foreign_licenses_info(
7848 self::get_all_licenses( $this->get_parent_id() )
7849 );
7850
7851 if ( ! empty ( $foreign_licenses ) ) {
7852 $foreign_licenses = array(
7853 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
7854 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
7855 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
7856 );
7857
7858 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
7859 }
7860
7861 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
7862
7863 if (
7864 ! $this->is_api_result_object( $result, 'licenses' ) ||
7865 ! is_array( $result->licenses ) ||
7866 empty( $result->licenses )
7867 ) {
7868 return null;
7869 }
7870
7871 $parent_license = null;
7872
7873 if ( empty( $license_key ) ) {
7874 $parent_license = $result->licenses[0];
7875 } else {
7876 foreach ( $result->licenses as $license ) {
7877 if ( $license_key === $license->secret_key ) {
7878 $parent_license = $license;
7879 break;
7880 }
7881 }
7882 }
7883
7884 if ( ! is_null( $parent_license ) ) {
7885 $parent_license = new FS_Plugin_License( $parent_license );
7886 }
7887
7888 return $parent_license;
7889 }
7890
7891 /**
7892 * @author Leo Fajardo (@leorw)
7893 * @since 2.3.0
7894 *
7895 * @return array
7896 */
7897 function get_sites_for_network_level_optin() {
7898 $sites = array();
7899 $all_sites = self::get_sites();
7900
7901 foreach ( $all_sites as $site ) {
7902 $blog_id = self::get_site_blog_id( $site );
7903
7904 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
7905 ! $this->is_installed_on_site( $blog_id )
7906 ) {
7907 $sites[] = $this->get_site_info( $site );
7908 }
7909 }
7910
7911 return $sites;
7912 }
7913
7914 /**
7915 * Delete account.
7916 *
7917 * @author Vova Feldman (@svovaf)
7918 * @since 1.0.3
7919 *
7920 * @param bool $check_user Enforce checking if user have plugins activation privileges.
7921 */
7922 function delete_account_event( $check_user = true ) {
7923 $this->_logger->entrance( 'slug = ' . $this->_slug );
7924
7925 if ( $check_user && ! $this->is_user_admin() ) {
7926 return;
7927 }
7928
7929 $this->do_action( 'before_account_delete' );
7930
7931 // Clear all admin notices.
7932 $this->_admin_notices->clear_all_sticky( false );
7933
7934 $this->_delete_site( false );
7935
7936 $delete_network_common_data = true;
7937
7938 if ( $this->_is_network_active ) {
7939 $installs = $this->get_blog_install_map();
7940
7941 // Don't delete common network data unless no other installs left.
7942 $delete_network_common_data = empty( $installs );
7943 }
7944
7945 if ( $delete_network_common_data ) {
7946 $this->_delete_plans( false );
7947
7948 $this->_delete_licenses( false );
7949
7950 // Delete add-ons related to plugin's account.
7951 $this->_delete_account_addons( false );
7952 }
7953
7954 // @todo Delete plans and licenses of add-ons.
7955
7956 self::$_accounts->store();
7957
7958 /**
7959 * IMPORTANT:
7960 * Clear crons must be executed before clearing all storage.
7961 * Otherwise, the cron will not be cleared.
7962 */
7963 if ( $delete_network_common_data ) {
7964 $this->clear_sync_cron();
7965 }
7966
7967 $this->clear_install_sync_cron();
7968
7969 // Clear all storage data.
7970 $this->_storage->clear_all( true, array(
7971 'is_delegated_connection',
7972 'connectivity_test',
7973 'is_on',
7974 ), false );
7975
7976 // Send delete event.
7977 $this->get_api_site_scope()->call( '/', 'delete' );
7978
7979 $this->do_action( 'after_account_delete' );
7980 }
7981
7982 /**
7983 * Delete network level account.
7984 *
7985 * @author Vova Feldman (@svovaf)
7986 * @since 2.0.0
7987 *
7988 * @param bool $check_user Enforce checking if user have plugins activation privileges.
7989 */
7990 function delete_network_account_event( $check_user = true ) {
7991 $this->_logger->entrance( 'slug = ' . $this->_slug );
7992
7993 if ( $check_user && ! $this->is_user_admin() ) {
7994 return;
7995 }
7996
7997 $this->do_action( 'before_network_account_delete' );
7998
7999 // Clear all admin notices.
8000 $this->_admin_notices->clear_all_sticky();
8001
8002 $this->_delete_plans( false, false );
8003
8004 $this->_delete_licenses( false );
8005
8006 // Delete add-ons related to plugin's account.
8007 $this->_delete_account_addons( false );
8008
8009 // @todo Delete plans and licenses of add-ons.
8010
8011 self::$_accounts->store( true );
8012
8013 /**
8014 * IMPORTANT:
8015 * Clear crons must be executed before clearing all storage.
8016 * Otherwise, the cron will not be cleared.
8017 */
8018 $this->clear_sync_cron( true );
8019 $this->clear_install_sync_cron( true );
8020
8021 $sites = self::get_sites();
8022
8023 $install_ids = array();
8024 foreach ( $sites as $site ) {
8025 $blog_id = self::get_site_blog_id( $site );
8026
8027 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8028 continue;
8029 }
8030
8031 $install_id = $this->_delete_site( true, $blog_id );
8032
8033 // Clear all storage data.
8034 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8035
8036 if ( FS_Site::is_valid_id( $install_id ) ) {
8037 $install_ids[] = $install_id;
8038 }
8039
8040 switch_to_blog( $blog_id );
8041
8042 $this->do_action( 'after_account_delete' );
8043
8044 restore_current_blog();
8045 }
8046
8047 $this->_storage->clear_all( true, array(
8048 'connectivity_test',
8049 'is_on',
8050 ), true );
8051
8052 // Send delete event.
8053 if ( ! empty( $install_ids ) ) {
8054 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8055 }
8056
8057 $this->do_action( 'after_network_account_delete' );
8058 }
8059
8060 /**
8061 * Plugin deactivation hook.
8062 *
8063 * @author Vova Feldman (@svovaf)
8064 * @since 1.0.1
8065 */
8066 function _deactivate_plugin_hook() {
8067 $this->_logger->entrance( 'slug = ' . $this->_slug );
8068
8069 if ( ! $this->is_user_admin() ) {
8070 return;
8071 }
8072
8073 $is_network_deactivation = fs_is_network_admin();
8074 $storage_keys_for_removal = array();
8075
8076 $this->_admin_notices->clear_all_sticky();
8077
8078 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8079 // Remember that plugin was already installed.
8080 $this->_storage->is_plugin_new_install = false;
8081 }
8082
8083 // Hook to plugin uninstall.
8084 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8085
8086 $this->clear_module_main_file_cache();
8087 $this->clear_sync_cron( $this->_is_network_active );
8088 $this->clear_install_sync_cron();
8089
8090 if ( $this->is_registered() ) {
8091 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8092 FS_Plugin_Updater::instance( $this )->delete_update_data();
8093 }
8094
8095 if ( $is_network_deactivation ) {
8096 // Send deactivation event.
8097 $this->sync_installs( array(
8098 'is_active' => false,
8099 ) );
8100 } else {
8101 // Send deactivation event.
8102 $this->sync_install( array(
8103 'is_active' => false,
8104 ) );
8105 }
8106 } else {
8107 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8108 // Reset connectivity test cache.
8109 $this->clear_connectivity_info();
8110
8111 $storage_keys_for_removal[] = 'connectivity_test';
8112 }
8113 }
8114
8115 if ( $is_network_deactivation ) {
8116
8117 if ( ! empty( $storage_keys_for_removal ) ) {
8118 $sites = self::get_sites();
8119
8120 foreach ( $sites as $site ) {
8121 $blog_id = self::get_site_blog_id( $site );
8122
8123 foreach ( $storage_keys_for_removal as $key ) {
8124 $this->_storage->remove( $key, false, $blog_id );
8125 }
8126
8127 $this->_storage->save( $blog_id );
8128 }
8129 }
8130 }
8131
8132 // Clear API cache on deactivation.
8133 FS_Api::clear_cache();
8134
8135 $this->remove_sdk_reference();
8136 }
8137
8138 /**
8139 * @author Vova Feldman (@svovaf)
8140 * @since 1.1.6
8141 */
8142 private function remove_sdk_reference() {
8143 global $fs_active_plugins;
8144
8145 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8146 if ( $this->_plugin_basename == $data->plugin_path ) {
8147 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8148 break;
8149 }
8150 }
8151
8152 fs_fallback_to_newest_active_sdk();
8153 }
8154
8155 /**
8156 * @author Vova Feldman (@svovaf)
8157 * @since 1.1.3
8158 *
8159 * @param bool $is_anonymous
8160 * @param bool|int $network_or_blog_id Since 2.0.0
8161 */
8162 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8163 // Store information regarding skip to try and opt-in the user
8164 // again in the future.
8165 $skip_info = array(
8166 'is' => $is_anonymous,
8167 'timestamp' => WP_FS__SCRIPT_START_TIME,
8168 'version' => $this->get_plugin_version(),
8169 );
8170
8171 if ( true === $network_or_blog_id ) {
8172 $this->_storage->is_anonymous_ms = $skip_info;
8173 } else {
8174 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8175 }
8176
8177 $this->network_upgrade_mode_completed();
8178
8179 // Update anonymous mode cache.
8180 $this->_is_anonymous = $is_anonymous;
8181 }
8182
8183 /**
8184 * @author Vova Feldman (@svovaf)
8185 * @since 2.5.1
8186 *
8187 * @param bool|int $network_or_blog_id
8188 */
8189 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8190 if ( true === $network_or_blog_id ) {
8191 unset( $this->_storage->is_anonymous_ms );
8192 } else {
8193 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8194 }
8195 }
8196
8197 /**
8198 * @author Vova Feldman (@svovaf)
8199 * @since 2.0.0
8200 *
8201 * @param int $blog_id Site ID.
8202 * @param int $user_id User ID.
8203 * @param string $domain Site domain.
8204 * @param string $path Site path.
8205 * @param int $network_id Network ID. Only relevant on multi-network installations.
8206 * @param array $meta Metadata. Used to set initial site options.
8207 *
8208 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8209 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8210 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8211 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8212 */
8213 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8214 $this->_logger->entrance();
8215
8216 if ( ! $this->_is_network_active ) {
8217 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8218 return;
8219 }
8220
8221 $site = null;
8222 $new_blog_id = $blog_id;
8223
8224 if ( $this->is_premium() &&
8225 $this->is_network_connected() &&
8226 is_object( $this->_license ) &&
8227 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8228 $this->is_license_network_active( $blog_id )
8229 ) {
8230 /**
8231 * 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.
8232 */
8233 $current_blog_id = get_current_blog_id();
8234 $license = clone $this->_license;
8235
8236 $this->switch_to_blog( $blog_id );
8237
8238 // Opt-in with network user.
8239 $this->install_with_user(
8240 $this->get_network_user(),
8241 $license->secret_key,
8242 false,
8243 false,
8244 false
8245 );
8246
8247 if ( is_object( $this->_site ) ) {
8248 if ( $this->_site->license_id == $license->id ) {
8249 /**
8250 * If the license was activated successfully, sync the license data from the remote server.
8251 */
8252 $this->_license = $license;
8253 $this->sync_site_license();
8254 }
8255 }
8256
8257 $site = $this->_site;
8258
8259 $this->switch_to_blog( $current_blog_id );
8260
8261 if ( is_object( $site ) ) {
8262 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8263
8264 // Already connected (with or without a license), so no need to continue.
8265 return;
8266 }
8267 }
8268
8269 if ( $this->is_network_anonymous() ) {
8270 /**
8271 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8272 */
8273 $this->skip_site_connection( $blog_id );
8274 } else if ( $this->is_network_delegated_connection() ) {
8275 /**
8276 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8277 */
8278 $this->delegate_site_connection( $blog_id );
8279 } else if ( $this->is_network_connected() ) {
8280 /**
8281 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8282 */
8283 $current_blog_id = get_current_blog_id();
8284
8285 $this->switch_to_blog( $blog_id );
8286
8287 // Opt-in with network user.
8288 $this->install_with_user(
8289 $this->get_network_user(),
8290 false,
8291 false,
8292 false,
8293 false
8294 );
8295
8296 $site = $this->_site;
8297
8298 $this->switch_to_blog( $current_blog_id );
8299 } else {
8300 /**
8301 * If the super-admin mixed different options (connect, skip, delegated):
8302 * a) If at least one site connection was delegated, then automatically delegate connection.
8303 * 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.
8304 */
8305 $has_delegated_site = false;
8306
8307 $sites = self::get_sites();
8308 foreach ( $sites as $wp_site ) {
8309 $blog_id = self::get_site_blog_id( $wp_site );
8310
8311 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8312 $has_delegated_site = true;
8313 break;
8314 }
8315 }
8316
8317 if ( $has_delegated_site ) {
8318 $this->delegate_site_connection( $blog_id );
8319 } else {
8320 $this->skip_site_connection( $blog_id );
8321 }
8322 }
8323
8324 /**
8325 * Store the new blog's information even if there's no install so that when a clone install is stored in the new blog's storage, we can try to resolve it automatically.
8326 *
8327 * @author Leo Fajardo (@leorw)
8328 * @since 2.5.0
8329 */
8330 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8331 }
8332
8333 /**
8334 * @author Vova Feldman (@svovaf)
8335 * @since 2.5.0
8336 *
8337 * @param \WP_Site $new_site
8338 * @param array $args
8339 */
8340 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8341 $this->_logger->entrance();
8342
8343 $this->_after_new_blog_callback(
8344 $new_site->id,
8345 // Dummy user ID (not in use).
8346 0,
8347 $new_site->domain,
8348 $new_site->path,
8349 $new_site->network_id,
8350 // Dummy meta, not in use.
8351 array()
8352 );
8353 }
8354
8355 /**
8356 * @author Vova Feldman (@svovaf)
8357 * @since 1.1.3
8358 *
8359 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8360 */
8361 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8362 if ( true === $network_or_blog_ids ) {
8363 $this->unset_anonymous_mode( true );
8364
8365 if ( fs_is_network_admin() ) {
8366 $this->_is_anonymous = null;
8367 }
8368
8369 // Rest anonymous mode for all non-delegated sub-sites.
8370 $blog_ids = $this->get_non_delegated_blog_ids();
8371 }
8372 else
8373 {
8374 if ( false === $network_or_blog_ids ) {
8375 $network_or_blog_ids = 0;
8376 }
8377
8378 $blog_ids = is_array( $network_or_blog_ids ) ?
8379 $network_or_blog_ids :
8380 array( $network_or_blog_ids );
8381
8382 foreach ( $blog_ids as $blog_id ) {
8383 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8384 $this->_is_anonymous = null;
8385 }
8386 }
8387 }
8388
8389 foreach ( $blog_ids as $blog_id ) {
8390 $this->unset_anonymous_mode( $blog_id );
8391 }
8392
8393 /**
8394 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8395 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8396 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8397 *
8398 * @author Leo Fajardo (@leorw)
8399 * @since 1.2.2
8400 */
8401 if ( ! $this->_is_network_active ) {
8402 $this->_is_anonymous = null;
8403 }
8404 }
8405
8406 /**
8407 * @author Leo Fajardo (@leorw)
8408 * @since 2.5.3
8409 */
8410 private function update_license_required_permissions_if_anonymous() {
8411 if ( ! $this->is_anonymous() ) {
8412 return;
8413 }
8414
8415 $this->reset_anonymous_mode( fs_is_network_admin() );
8416
8417 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8418 'essentials' => true,
8419 'events' => true,
8420 'diagnostic' => false,
8421 'extensions' => false,
8422 'site' => false,
8423 ) );
8424 }
8425
8426 /**
8427 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8428 * deleting the account in the network level, the URL of the page to redirect to is correct.
8429 *
8430 * @author Leo Fajardo (@leorw)
8431 *
8432 * @since 2.1.3
8433 */
8434 private function maybe_set_slug_and_network_menu_exists_flag() {
8435 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8436 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8437 $this->_menu->get_slug() :
8438 $this->_slug
8439 );
8440 }
8441 }
8442
8443 /**
8444 * Clears the anonymous mode and redirects to the opt-in screen.
8445 *
8446 * @author Vova Feldman (@svovaf)
8447 * @since 1.1.7
8448 */
8449 function connect_again() {
8450 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8451 return;
8452 }
8453
8454 if ( $this->is_anonymous() ) {
8455 $this->reset_anonymous_mode( fs_is_network_admin() );
8456 }
8457
8458 $activation_url_params = array();
8459
8460 if ( $this->is_pending_activation() ) {
8461 $this->clear_pending_activation_mode();
8462
8463 if ( fs_request_get_bool( 'require_license' ) ) {
8464 $activation_url_params['require_license'] = true;
8465 }
8466 }
8467
8468 $this->maybe_set_slug_and_network_menu_exists_flag();
8469
8470 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8471 }
8472
8473 /**
8474 * Skip account connect, and set anonymous mode.
8475 *
8476 * @author Vova Feldman (@svovaf)
8477 * @since 1.1.1
8478 *
8479 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8480 */
8481 function skip_connection( $network_or_blog_ids = false ) {
8482 $this->_logger->entrance();
8483
8484 $this->_admin_notices->remove_sticky( 'connect_account' );
8485
8486 if ( true === $network_or_blog_ids ) {
8487 $this->set_anonymous_mode( true, true );
8488
8489 if ( fs_is_network_admin() ) {
8490 $this->_is_anonymous = null;
8491 }
8492
8493 // Rest anonymous mode for all non-delegated sub-sites.
8494 $blog_ids = $this->get_non_delegated_blog_ids();
8495 }
8496 else
8497 {
8498 if ( false === $network_or_blog_ids ) {
8499 $network_or_blog_ids = 0;
8500 }
8501
8502 $blog_ids = is_array( $network_or_blog_ids ) ?
8503 $network_or_blog_ids :
8504 array( $network_or_blog_ids );
8505
8506 foreach ( $blog_ids as $blog_id ) {
8507 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8508 $this->_is_anonymous = null;
8509 }
8510 }
8511 }
8512
8513 foreach ( $blog_ids as $blog_id ) {
8514 $this->skip_site_connection( $blog_id );
8515 }
8516
8517 $this->network_upgrade_mode_completed();
8518 }
8519
8520 /**
8521 * Skip connection for specific site in the network.
8522 *
8523 * @author Vova Feldman (@svovaf)
8524 * @since 2.0.0
8525 *
8526 * @param int|null $blog_id
8527 * @param bool $send_skip
8528 */
8529 private function skip_site_connection( $blog_id = null ) {
8530 $this->_logger->entrance();
8531
8532 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8533
8534 $this->set_anonymous_mode( true, $blog_id );
8535 }
8536
8537 /**
8538 * Plugin version update hook.
8539 *
8540 * @author Vova Feldman (@svovaf)
8541 * @since 1.0.4
8542 */
8543 private function update_plugin_version_event() {
8544 $this->_logger->entrance();
8545
8546 if ( ! $this->is_registered() ) {
8547 return;
8548 }
8549
8550 $this->schedule_install_sync();
8551 // $this->sync_install( array(), true );
8552 }
8553
8554 /**
8555 * Generate an MD5 signature of a plugins collection.
8556 * This helper methods used to identify changes in a plugins collection.
8557 *
8558 * @author Vova Feldman (@svovaf)
8559 * @since 2.0.0
8560 *
8561 * @param array [string]array $plugins
8562 *
8563 * @return string
8564 */
8565 private function get_plugins_thumbprint( $plugins ) {
8566 ksort( $plugins );
8567
8568 $thumbprint = '';
8569 foreach ( $plugins as $basename => $data ) {
8570 $thumbprint .= $data['slug'] . ',' .
8571 $data['Version'] . ',' .
8572 ( $data['is_active'] ? '1' : '0' ) . ';';
8573 }
8574
8575 return md5( $thumbprint );
8576 }
8577
8578 /**
8579 * Return a list of modified plugins since the last sync.
8580 *
8581 * Note:
8582 * There's no point to store a plugins counter since even if the number of
8583 * plugins didn't change, we still need to check if the versions are all the
8584 * same and the activity state is similar.
8585 *
8586 * @author Vova Feldman (@svovaf)
8587 * @since 1.1.8
8588 *
8589 * @return array|false
8590 */
8591 private function get_plugins_data_for_api() {
8592 // Alias.
8593 $site_active_plugins_option_name = 'active_plugins';
8594 $network_plugins_option_name = 'all_plugins';
8595
8596 /**
8597 * Collection of all site level active plugins.
8598 */
8599 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8600
8601 if ( ! is_object( $site_active_plugins_cache ) ) {
8602 $site_active_plugins_cache = (object) array(
8603 'timestamp' => '',
8604 'md5' => '',
8605 'plugins' => array(),
8606 );
8607 }
8608
8609 $time = time();
8610
8611 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8612 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8613 ) {
8614 // Don't send plugin updates if last update was in the past 5 min.
8615 return false;
8616 }
8617
8618 // Write timestamp to lock the logic.
8619 $site_active_plugins_cache->timestamp = $time;
8620 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8621
8622 // Reload options from DB.
8623 self::$_accounts->load( true );
8624 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8625
8626 if ( $time != $site_active_plugins_cache->timestamp ) {
8627 // If timestamp is different, then another thread captured the lock.
8628 return false;
8629 }
8630
8631 /**
8632 * Collection of all plugins (network level).
8633 */
8634 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8635
8636 if ( ! is_object( $network_plugins_cache ) ) {
8637 $network_plugins_cache = (object) array(
8638 'timestamp' => '',
8639 'md5' => '',
8640 'plugins' => array(),
8641 );
8642 }
8643
8644 // Check if there's a change in plugins.
8645 $network_plugins = self::get_network_plugins();
8646 $site_active_plugins = self::get_site_active_plugins();
8647
8648 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8649 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8650
8651 // Check if plugins status changed (version or active/inactive).
8652 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8653 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8654
8655 if ( ! $network_plugins_changed &&
8656 ! $site_active_plugins_changed
8657 ) {
8658 // No changes.
8659 return array();
8660 }
8661
8662 $plugins_update_data = array();
8663
8664 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8665 if ( ! isset( $network_plugins[ $basename ] ) ) {
8666 // Plugin uninstalled.
8667 $uninstalled_plugin_data = $data;
8668 $uninstalled_plugin_data['is_active'] = false;
8669 $uninstalled_plugin_data['is_uninstalled'] = true;
8670 $plugins_update_data[] = $uninstalled_plugin_data;
8671
8672 unset( $network_plugins[ $basename ] );
8673
8674 unset( $network_plugins_cache->plugins[ $basename ] );
8675 unset( $site_active_plugins_cache->plugins[ $basename ] );
8676
8677 continue;
8678 }
8679
8680 $was_active = $data['is_active'] ||
8681 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8682 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8683 $is_active = $network_plugins[ $basename ]['is_active'] ||
8684 ( isset( $site_active_plugins[ $basename ] ) &&
8685 $site_active_plugins[ $basename ]['is_active'] );
8686
8687 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8688 isset( $site_active_plugins[ $basename ] )
8689 ) {
8690 // Plugin was site level activated.
8691 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
8692 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8693 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8694 ! isset( $site_active_plugins[ $basename ] )
8695 ) {
8696 // Plugin was site level deactivated.
8697 unset( $site_active_plugins_cache->plugins[ $basename ] );
8698 }
8699
8700 $prev_version = $data['version'];
8701 $current_version = $network_plugins[ $basename ]['Version'];
8702
8703 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8704 // Plugin activated or deactivated, or version changed.
8705
8706 if ( $was_active !== $is_active ) {
8707 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8708 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8709 }
8710 }
8711
8712 if ( $prev_version !== $current_version ) {
8713 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8714 }
8715
8716 $updated_plugin_data = $data;
8717 $updated_plugin_data['is_active'] = $is_active;
8718 $updated_plugin_data['version'] = $current_version;
8719 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8720 $plugins_update_data[] = $updated_plugin_data;
8721 }
8722 }
8723
8724 // Find new plugins that weren't yet seen before.
8725 foreach ( $network_plugins as $basename => $data ) {
8726 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8727 // New plugin.
8728 $new_plugin = array(
8729 'slug' => $data['slug'],
8730 'version' => $data['Version'],
8731 'title' => $data['Name'],
8732 'is_active' => $data['is_active'],
8733 'is_uninstalled' => false,
8734 );
8735
8736 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8737
8738 $is_site_level_active = (
8739 isset( $site_active_plugins[ $basename ] ) &&
8740 $site_active_plugins[ $basename ]['is_active']
8741 );
8742
8743 /**
8744 * If not network active, set the activity status based on the site-level plugin status.
8745 */
8746 if ( ! $new_plugin['is_active'] ) {
8747 $new_plugin['is_active'] = $is_site_level_active;
8748 }
8749
8750 $plugins_update_data[] = $new_plugin;
8751
8752 if ( isset( $site_active_plugins[ $basename ] ) ) {
8753 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8754 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
8755 }
8756 }
8757 }
8758
8759 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8760 $site_active_plugins_cache->timestamp = $time;
8761 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8762
8763 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8764 $network_plugins_cache->timestamp = $time;
8765 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8766
8767 return $plugins_update_data;
8768 }
8769
8770 /**
8771 * Return a list of modified themes since the last sync.
8772 *
8773 * Note:
8774 * There's no point to store a themes counter since even if the number of
8775 * themes didn't change, we still need to check if the versions are all the
8776 * same and the activity state is similar.
8777 *
8778 * @author Vova Feldman (@svovaf)
8779 * @since 1.1.8
8780 *
8781 * @return array|false
8782 */
8783 private function get_themes_data_for_api() {
8784 // Alias.
8785 $option_name = 'all_themes';
8786
8787 $all_cached_themes = self::$_accounts->get_option( $option_name );
8788
8789 if ( ! is_object( $all_cached_themes ) ) {
8790 $all_cached_themes = (object) array(
8791 'timestamp' => '',
8792 'md5' => '',
8793 'themes' => array(),
8794 );
8795 }
8796
8797 $time = time();
8798
8799 if ( ! empty( $all_cached_themes->timestamp ) &&
8800 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8801 ) {
8802 // Don't send theme updates if last update was in the past 5 min.
8803 return false;
8804 }
8805
8806 // Write timestamp to lock the logic.
8807 $all_cached_themes->timestamp = $time;
8808 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8809
8810 // Reload options from DB.
8811 self::$_accounts->load( true );
8812 $all_cached_themes = self::$_accounts->get_option( $option_name );
8813
8814 if ( $time != $all_cached_themes->timestamp ) {
8815 // If timestamp is different, then another thread captured the lock.
8816 return false;
8817 }
8818
8819 // Get active theme.
8820 $active_theme = wp_get_theme();
8821 $active_theme_stylesheet = $active_theme->get_stylesheet();
8822
8823 // Check if there's a change in themes.
8824 $all_themes = wp_get_themes();
8825
8826 // Check if themes changed.
8827 ksort( $all_themes );
8828
8829 $themes_signature = '';
8830 foreach ( $all_themes as $slug => $data ) {
8831 $is_active = ( $slug === $active_theme_stylesheet );
8832 $themes_signature .= $slug . ',' .
8833 $data->version . ',' .
8834 ( $is_active ? '1' : '0' ) . ';';
8835 }
8836
8837 // Check if themes status changed (version or active/inactive).
8838 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8839
8840 $themes_update_data = array();
8841
8842 if ( $themes_changed ) {
8843 // Change in themes, report changes.
8844
8845 // Update existing themes info.
8846 foreach ( $all_cached_themes->themes as $slug => $data ) {
8847 $is_active = ( $slug === $active_theme_stylesheet );
8848
8849 if ( ! isset( $all_themes[ $slug ] ) ) {
8850 // Plugin uninstalled.
8851 $uninstalled_theme_data = $data;
8852 $uninstalled_theme_data['is_active'] = false;
8853 $uninstalled_theme_data['is_uninstalled'] = true;
8854 $themes_update_data[] = $uninstalled_theme_data;
8855
8856 unset( $all_themes[ $slug ] );
8857 unset( $all_cached_themes->themes[ $slug ] );
8858 } else if ( $data['is_active'] !== $is_active ||
8859 $data['version'] !== $all_themes[ $slug ]->version
8860 ) {
8861 // Plugin activated or deactivated, or version changed.
8862
8863 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
8864 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
8865
8866 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
8867 }
8868 }
8869
8870 // Find new themes that weren't yet seen before.
8871 foreach ( $all_themes as $slug => $data ) {
8872 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
8873 $is_active = ( $slug === $active_theme_stylesheet );
8874
8875 // New plugin.
8876 $new_plugin = array(
8877 'slug' => $slug,
8878 'version' => $data->version,
8879 'title' => $data->name,
8880 'is_active' => $is_active,
8881 'is_uninstalled' => false,
8882 );
8883
8884 $themes_update_data[] = $new_plugin;
8885 $all_cached_themes->themes[ $slug ] = $new_plugin;
8886 }
8887 }
8888
8889 $all_cached_themes->md5 = md5( $themes_signature );
8890 $all_cached_themes->timestamp = time();
8891 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8892 }
8893
8894 return $themes_update_data;
8895 }
8896
8897 /**
8898 * Get site data for API install request.
8899 *
8900 * @author Vova Feldman (@svovaf)
8901 * @since 1.1.2
8902 *
8903 * @param string[] $override
8904 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
8905 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
8906 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
8907 *
8908 * @return array
8909 */
8910 private function get_install_data_for_api(
8911 array $override,
8912 $include_plugins = true,
8913 $include_themes = true,
8914 $include_blog_data = true
8915 ) {
8916 // Alias.
8917 $permissions = FS_Permission_Manager::instance( $this );
8918
8919 if ( $permissions->is_extensions_tracking_allowed() ) {
8920 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
8921 /**
8922 * @since 1.1.8 Also send plugin updates.
8923 */
8924 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
8925 $plugins = $this->get_plugins_data_for_api();
8926 if ( ! empty( $plugins ) ) {
8927 $override['plugins'] = $plugins;
8928 }
8929 }
8930 }
8931
8932 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
8933 /**
8934 * @since 1.1.8 Also send themes updates.
8935 */
8936 if ( $include_themes && ! isset( $override['themes'] ) ) {
8937 $themes = $this->get_themes_data_for_api();
8938 if ( ! empty( $themes ) ) {
8939 $override['themes'] = $themes;
8940 }
8941 }
8942 }
8943 }
8944
8945 $versions = $this->get_versions();
8946
8947 $blog_data = array();
8948 if ( $include_blog_data ) {
8949 $blog_data['url'] = self::get_unfiltered_site_url();
8950
8951 if ( $permissions->is_diagnostic_tracking_allowed() ) {
8952 $blog_data = array_merge( $blog_data, array(
8953 'language' => self::get_sanitized_language(),
8954 'title' => get_bloginfo( 'name' ),
8955 ) );
8956 }
8957 }
8958
8959 return array_merge( $versions, $blog_data, array(
8960 'version' => $this->get_plugin_version(),
8961 'is_premium' => $this->is_premium(),
8962 // Special params.
8963 'is_active' => true,
8964 'is_uninstalled' => false,
8965 ), $override );
8966 }
8967
8968 /**
8969 * Update installs details.
8970 *
8971 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
8972 *
8973 * @author Vova Feldman (@svovaf)
8974 * @since 2.0.0
8975 *
8976 * @param string[] string $override
8977 * @param bool $only_diff
8978 * @param bool $is_keepalive
8979 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
8980 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
8981 *
8982 * @return array
8983 */
8984 private function get_installs_data_for_api(
8985 array $override,
8986 $only_diff = false,
8987 $is_keepalive = false,
8988 $include_plugins = true,
8989 $include_themes = true
8990 ) {
8991 /**
8992 * @since 1.1.8 Also send plugin updates.
8993 */
8994 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
8995 // $plugins = $this->get_plugins_data_for_api();
8996 // if ( ! empty( $plugins ) ) {
8997 // $override['plugins'] = $plugins;
8998 // }
8999 // }
9000 /**
9001 * @since 1.1.8 Also send themes updates.
9002 */
9003 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9004 // $themes = $this->get_themes_data_for_api();
9005 // if ( ! empty( $themes ) ) {
9006 // $override['themes'] = $themes;
9007 // }
9008 // }
9009
9010 // Common properties.
9011 $versions = $this->get_versions();
9012 $common = array_merge( $versions, array(
9013 'version' => $this->get_plugin_version(),
9014 'is_premium' => $this->is_premium(),
9015 ), $override );
9016
9017
9018 $is_common_diff_for_any_site = false;
9019 $common_diff_union = array();
9020
9021 $installs_data = array();
9022
9023 $sites = self::get_sites();
9024
9025 $subsite_data_for_api_by_install_id = array();
9026 $install_url_by_install_id = array();
9027 $subsite_registration_date_by_install_id = array();
9028
9029 foreach ( $sites as $site ) {
9030 $blog_id = self::get_site_blog_id( $site );
9031
9032 $install = $this->get_install_by_blog_id( $blog_id );
9033
9034 if ( is_object( $install ) ) {
9035 if ( $install->user_id != $this->_user->id ) {
9036 // Install belongs to a different owner.
9037 continue;
9038 }
9039
9040 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9041 // Don't send updates regarding opted-out installs.
9042 continue;
9043 }
9044
9045 $install_data = $this->get_site_info( $site, true );
9046
9047 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9048 continue;
9049 }
9050
9051 $uid = $install_data['uid'];
9052 $url = $install_data['url'];
9053 $registration_date = $install_data['registration_date'];
9054
9055 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9056 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9057 $clone_install_url = $install_url_by_install_id[ $install->id ];
9058 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9059
9060 $skip = false;
9061
9062 if (
9063 ! empty( $install_data['registration_date'] ) &&
9064 ! empty( $clone_subsite_registration_date )
9065 ) {
9066 /**
9067 * If the current subsite was created after the other subsite that is also linked to the same install ID, we assume that it's a clone (not the original), and therefore, would skip its processing.
9068 *
9069 * @author Leo Fajardo (@leorw)
9070 * @since 2.5.1
9071 */
9072 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9073 } else if (
9074 /**
9075 * If we already have an install with the same URL as the subsite it's stored in, skip the current subsite. Otherwise, replace the existing install's data with the current subsite's install's data if the URLs match.
9076 *
9077 * @author Leo Fajardo (@leorw)
9078 * @since 2.5.0
9079 */
9080 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9081 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9082 ) {
9083 $skip = true;
9084 }
9085
9086 if ( $skip ) {
9087 // Store the skipped subsite's ID so that the clone resolution manager can try to resolve the clone install that is stored in that subsite later on.
9088 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9089 continue;
9090 }
9091 }
9092
9093 unset( $install_data['blog_id'] );
9094 unset( $install_data['uid'] );
9095 unset( $install_data['url'] );
9096 unset( $install_data['registration_date'] );
9097
9098 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9099 $install_data['is_uninstalled'] = $install->is_uninstalled;
9100
9101 $common_diff = null;
9102 $is_common_diff = false;
9103 if ( $only_diff ) {
9104 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9105 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9106
9107 $is_common_diff = ! empty( $common_diff );
9108
9109 if ( $is_common_diff ) {
9110 foreach ( $common_diff as $k => $v ) {
9111 if ( ! isset( $common_diff_union[ $k ] ) ) {
9112 $common_diff_union[ $k ] = $v;
9113 }
9114 }
9115 }
9116
9117 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9118 }
9119
9120 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9121 // Add install ID and site unique ID.
9122 $install_data['id'] = $install->id;
9123 $install_data['uid'] = $uid;
9124 $install_data['url'] = $url;
9125
9126 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9127 $install_url_by_install_id[ $install->id ] = $install->url;
9128 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9129 }
9130 }
9131 }
9132
9133 restore_current_blog();
9134
9135 $installs_data = array_merge(
9136 $installs_data,
9137 array_values( $subsite_data_for_api_by_install_id )
9138 );
9139
9140 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9141 if ( ! $only_diff ) {
9142 $installs_data[] = $common;
9143 } else if ( ! empty( $common_diff_union ) ) {
9144 $installs_data[] = $common_diff_union;
9145 }
9146 }
9147
9148 foreach ( $installs_data as &$data ) {
9149 $data = (object) $data;
9150 }
9151
9152 return $installs_data;
9153 }
9154
9155 /**
9156 * Compare site actual data to the stored install data and return the differences for an API data sync.
9157 *
9158 * @author Vova Feldman (@svovaf)
9159 * @since 2.0.0
9160 *
9161 * @param array $site
9162 * @param FS_Site $install
9163 * @param string[] string $override
9164 *
9165 * @return array
9166 */
9167 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9168 $diff = array();
9169 $special = array();
9170 $special_override = false;
9171
9172 foreach ( $site as $p => $v ) {
9173 if ( property_exists( $install, $p ) ) {
9174 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9175 $install->{$p} != $v
9176 ) {
9177 $val = self::get_api_sanitized_property( $p, $v );
9178
9179 if ( $install->{$p} != $val ) {
9180 $install->{$p} = $val;
9181 $diff[ $p ] = $val;
9182 }
9183 }
9184 } else {
9185 $special[ $p ] = $v;
9186
9187 if ( isset( $override[ $p ] ) ||
9188 'plugins' === $p ||
9189 'themes' === $p
9190 ) {
9191 $special_override = true;
9192 }
9193 }
9194 }
9195
9196 if ( $special_override || 0 < count( $diff ) ) {
9197 // Add special params only if has at least one
9198 // standard param, or if explicitly requested to
9199 // override a special param or a param which is not exist
9200 // in the install object.
9201 $diff = array_merge( $diff, $special );
9202 }
9203
9204 return $diff;
9205 }
9206
9207 /**
9208 * @author Leo Fajardo (@leorw)
9209 * @since 2.5.1
9210 */
9211 private function send_pending_clone_update_once() {
9212 $this->_logger->entrance();
9213
9214 if ( ! empty( $this->_storage->clone_id ) ) {
9215 return;
9216 }
9217
9218 $install_clone = $this->get_api_site_scope()->call(
9219 '/clones',
9220 'post',
9221 array( 'site_url' => self::get_unfiltered_site_url() )
9222 );
9223
9224 if ( $this->is_api_result_entity( $install_clone ) ) {
9225 $this->_storage->clone_id = $install_clone->id;
9226 }
9227 }
9228
9229 /**
9230 * @author Leo Fajardo (@leorw)
9231 * @since 2.5.1
9232 *
9233 * @param string $resolution_type
9234 * @param FS_Site $clone_context_install
9235 */
9236 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9237 $this->_logger->entrance();
9238
9239 if ( empty( $this->_storage->clone_id ) ) {
9240 return;
9241 }
9242
9243 $new_install_id = null;
9244 $current_site = null;
9245
9246 $flush = false;
9247
9248 /**
9249 * If the current site is now different from the context install before the clone resolution, we need to override `$this->_site` so that the API call below will be made with the right install scope entity.
9250 */
9251 if ( $clone_context_install->id != $this->_site->id ) {
9252 $new_install_id = $this->_site->id;
9253 $current_site = $this->_site;
9254 $this->_site = $clone_context_install;
9255
9256 $flush = true;
9257 }
9258
9259 $this->get_api_site_scope( $flush )->call(
9260 "/clones/{$this->_storage->clone_id}",
9261 'put',
9262 array(
9263 'resolution' => $resolution_type,
9264 'new_install_id' => $new_install_id,
9265 )
9266 );
9267
9268 if ( is_object( $current_site ) ) {
9269 /**
9270 * Ensure that the install scope entity is updated back to the previous install entity.
9271 */
9272 $this->_site = $current_site;
9273
9274 // Restore the previous install scope entity of the API.
9275 $this->get_api_site_scope( true );
9276 }
9277 }
9278
9279 /**
9280 * Update install only if changed.
9281 *
9282 * @author Vova Feldman (@svovaf)
9283 * @since 1.0.9
9284 *
9285 * @param string[] string $override
9286 * @param bool $flush
9287 * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
9288 *
9289 * @return false|object|string
9290 */
9291 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9292 $this->_logger->entrance();
9293
9294 $check_properties = $this->get_install_data_for_api( $override );
9295
9296 if ( $flush ) {
9297 $params = $check_properties;
9298 } else {
9299 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9300 }
9301
9302 if ( empty( $params ) ) {
9303 $keepalive_only_update = $this->should_send_keepalive_update();
9304
9305 if ( ! $keepalive_only_update ) {
9306 /**
9307 * There are no updates to send including keepalive.
9308 *
9309 * @author Leo Fajardo (@leorw)
9310 * @since 2.2.3
9311 */
9312 return false;
9313 }
9314 }
9315
9316 if ( $is_two_way_sync ) {
9317 /**
9318 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9319 *
9320 * @author Leo Fajardo (@leorw)
9321 * @since 2.2.3
9322 */
9323 if ( ! is_multisite() ) {
9324 // Update last install sync timestamp.
9325 $this->set_cron_execution_timestamp( 'install_sync' );
9326 }
9327
9328 $params['uid'] = $this->get_anonymous_id();
9329 }
9330
9331 $this->set_keepalive_timestamp();
9332
9333 // Send updated values to FS.
9334 $site = $this->api_site_call( '/', 'put', $params, true );
9335
9336 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9337 /**
9338 * Clear scheduled install sync after a two-way sync call.
9339 *
9340 * @author Leo Fajardo (@leorw)
9341 * @since 2.2.3
9342 */
9343 if ( ! is_multisite() ) {
9344 // I successfully sent install update, clear scheduled sync if exist.
9345 $this->clear_install_sync_cron();
9346 }
9347 }
9348
9349 return $site;
9350 }
9351
9352 /**
9353 * Update installs only if changed.
9354 *
9355 * @author Vova Feldman (@svovaf)
9356 * @since 2.0.0
9357 *
9358 * @param string[] string $override
9359 * @param bool $flush
9360 * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
9361 *
9362 * @return false|object|string
9363 */
9364 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9365 $this->_logger->entrance();
9366
9367 /**
9368 * Pass `true` to use the network level storage since the update is for many installs.
9369 *
9370 * @author Leo Fajardo (@leorw)
9371 * @since 2.2.3
9372 */
9373 $should_send_keepalive = $this->should_send_keepalive_update( true );
9374
9375 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9376
9377 if ( empty( $installs_data ) ) {
9378 return false;
9379 }
9380
9381 if ( $is_two_way_sync ) {
9382 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9383 $this->set_cron_execution_timestamp( 'install_sync' );
9384 }
9385
9386 /**
9387 * Pass `true` to use the network level storage since the update is for many installs.
9388 *
9389 * @author Leo Fajardo (@leorw)
9390 * @since 2.2.3
9391 */
9392 $this->set_keepalive_timestamp( true );
9393
9394 // Send updated values to FS.
9395 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9396
9397 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9398 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9399 $this->clear_install_sync_cron();
9400 }
9401
9402 return $result;
9403 }
9404
9405 /**
9406 * @author Leo Fajardo (@leorw)
9407 *
9408 * @param bool|null $use_network_level_storage
9409 *
9410 * @return bool
9411 */
9412 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9413 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9414
9415 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9416 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9417 return true;
9418 } else {
9419 // 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.
9420 return ( 7 == rand( 1, 7 ) );
9421 }
9422 }
9423
9424 /**
9425 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9426 *
9427 * @author Leo Fajardo (@leorw)
9428 * @since 2.3.2
9429 */
9430 private function maybe_sync_install_user() {
9431 if ( $this->_user->id == $this->_site->user_id ) {
9432 return;
9433 }
9434
9435 // Fetch user data and store if found.
9436 $this->sync_user_by_current_install();
9437 }
9438
9439 /**
9440 * Update install only if changed.
9441 *
9442 * @author Vova Feldman (@svovaf)
9443 * @since 1.0.9
9444 *
9445 * @param string[] string $override
9446 * @param bool $flush
9447 */
9448 function sync_install( $override = array(), $flush = false ) {
9449 $this->_logger->entrance();
9450
9451 $site = $this->send_install_update( $override, $flush, true );
9452
9453 if ( false === $site ) {
9454 // No sync required.
9455 return;
9456 }
9457
9458 if ( ! $this->is_api_result_entity( $site ) ) {
9459 // Failed to sync, don't update locally.
9460 return;
9461 }
9462
9463 $this->_site = new FS_Site( $site );
9464
9465 $this->_store_site( true );
9466 }
9467
9468 /**
9469 * Update install only if changed.
9470 *
9471 * @author Vova Feldman (@svovaf)
9472 * @since 1.0.9
9473 *
9474 * @param string[] string $override
9475 * @param bool $flush
9476 */
9477 private function sync_installs( $override = array(), $flush = false ) {
9478 $this->_logger->entrance();
9479
9480 $result = $this->send_installs_update( $override, $flush, true );
9481
9482 if ( false === $result ) {
9483 // No sync required.
9484 return;
9485 }
9486
9487 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9488 // Failed to sync, don't update locally.
9489 return;
9490 }
9491
9492 $address_to_blog_map = $this->get_address_to_blog_map();
9493
9494 foreach ( $result->installs as $install ) {
9495 $this->_site = new FS_Site( $install );
9496
9497 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9498 $blog_id = $address_to_blog_map[ $address ];
9499
9500 $this->_store_site( true, $blog_id );
9501 }
9502 }
9503
9504 /**
9505 * Track install's custom event.
9506 *
9507 * IMPORTANT:
9508 * Custom event tracking is currently only supported for specific clients.
9509 * If you are not one of them, please don't use this method. If you will,
9510 * the API will simply ignore your request based on the plugin ID.
9511 *
9512 * Need custom tracking for your plugin or theme?
9513 * If you are interested in custom event tracking please contact yo@freemius.com
9514 * for further details.
9515 *
9516 * @author Vova Feldman (@svovaf)
9517 * @since 1.2.1
9518 *
9519 * @param string $name Event name.
9520 * @param array $properties Associative key/value array with primitive values only
9521 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9522 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9523 *
9524 * @return object|false Event data or FALSE on failure.
9525 *
9526 * @throws \Freemius_InvalidArgumentException
9527 */
9528 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9529 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9530
9531 if ( ! $this->is_registered() ) {
9532 return false;
9533 }
9534
9535 $event = array( 'type' => $name );
9536
9537 if ( is_numeric( $process_at ) && $process_at > time() ) {
9538 $event['process_at'] = $process_at;
9539 }
9540
9541 if ( $once ) {
9542 $event['once'] = true;
9543 }
9544
9545 if ( ! empty( $properties ) ) {
9546 // Verify associative array values are primitive.
9547 foreach ( $properties as $k => $v ) {
9548 if ( ! is_scalar( $v ) ) {
9549 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9550 }
9551 }
9552
9553 $event['properties'] = $properties;
9554 }
9555
9556 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9557
9558 return $this->is_api_error( $result ) ?
9559 false :
9560 $result;
9561 }
9562
9563 /**
9564 * Track install's custom event only once, but it still triggers the API call.
9565 *
9566 * IMPORTANT:
9567 * Custom event tracking is currently only supported for specific clients.
9568 * If you are not one of them, please don't use this method. If you will,
9569 * the API will simply ignore your request based on the plugin ID.
9570 *
9571 * Need custom tracking for your plugin or theme?
9572 * If you are interested in custom event tracking please contact yo@freemius.com
9573 * for further details.
9574 *
9575 * @author Vova Feldman (@svovaf)
9576 * @since 1.2.1
9577 *
9578 * @param string $name Event name.
9579 * @param array $properties Associative key/value array with primitive values only
9580 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9581 *
9582 * @return object|false Event data or FALSE on failure.
9583 *
9584 * @throws \Freemius_InvalidArgumentException
9585 *
9586 * @user Freemius::track_event()
9587 */
9588 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9589 return $this->track_event( $name, $properties, $process_at, true );
9590 }
9591
9592 /**
9593 * Plugin uninstall hook.
9594 *
9595 * @author Vova Feldman (@svovaf)
9596 * @since 1.0.1
9597 *
9598 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9599 */
9600 function _uninstall_plugin_event( $check_user = true ) {
9601 $this->_logger->entrance( 'slug = ' . $this->_slug );
9602
9603 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9604 return;
9605 }
9606
9607 $params = array();
9608 $uninstall_reason = null;
9609 if ( isset( $this->_storage->uninstall_reason ) ) {
9610 $uninstall_reason = $this->_storage->uninstall_reason;
9611 $params['reason_id'] = $uninstall_reason->id;
9612 $params['reason_info'] = $uninstall_reason->info;
9613 }
9614
9615 if ( ! $this->is_registered() ) {
9616 // Send anonymous uninstall event only if user submitted a feedback.
9617 if ( isset( $uninstall_reason ) ) {
9618 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9619 $this->opt_in( false, false, false, false, true );
9620 } else {
9621 $params['uid'] = $this->get_anonymous_id();
9622 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9623 }
9624 }
9625 } else {
9626 $params = array_merge( $params, array(
9627 'is_active' => false,
9628 'is_uninstalled' => true,
9629 ) );
9630
9631 if ( $this->_is_network_active ) {
9632 // Send uninstall event.
9633 $this->send_installs_update( $params );
9634 } else {
9635 // Send uninstall event and handle the result.
9636 $this->sync_install( $params );
9637 }
9638 }
9639
9640 // @todo Decide if we want to delete plugin information from db.
9641 }
9642
9643 /**
9644 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9645 *
9646 * @author Vova Feldman (@svovaf)
9647 * @since 2.2.1
9648 *
9649 * @param string $is_premium
9650 * @param string $caller
9651 *
9652 * @return void
9653 */
9654 function set_basename( $is_premium, $caller ) {
9655 $basename = plugin_basename( $caller );
9656
9657 $current_basename = $is_premium ?
9658 $this->_premium_plugin_basename :
9659 $this->_free_plugin_basename;
9660
9661 if ( $current_basename == $basename ) {
9662 // Basename value set correctly.
9663 return;
9664 }
9665
9666 if ( $is_premium ) {
9667 $this->_premium_plugin_basename = $basename;
9668 } else {
9669 $this->_free_plugin_basename = $basename;
9670 }
9671
9672 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9673
9674 register_activation_hook(
9675 $plugin_dir . $basename,
9676 array( &$this, '_activate_plugin_event_hook' )
9677 );
9678 }
9679
9680 /**
9681 * @author Vova Feldman (@svovaf)
9682 * @since 1.1.1
9683 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9684 *
9685 * @return string
9686 */
9687 function premium_plugin_basename() {
9688 if ( ! isset( $this->_premium_plugin_basename ) ) {
9689 $this->_premium_plugin_basename = $this->is_premium() ?
9690 // The product is premium, so use the current basename.
9691 $this->_plugin_basename :
9692 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9693 }
9694
9695 return $this->_premium_plugin_basename;
9696 }
9697
9698 /**
9699 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9700 *
9701 * @author Vova Feldman (@svovaf)
9702 * @since 1.0.2
9703 */
9704 public static function _uninstall_plugin_hook() {
9705 self::_load_required_static();
9706
9707 self::$_static_logger->entrance();
9708
9709 if ( ! current_user_can( 'activate_plugins' ) ) {
9710 return;
9711 }
9712
9713 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9714
9715 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9716
9717 define( 'WP_FS__UNINSTALL_MODE', true );
9718
9719 $fs = self::get_instance_by_file( $plugin_file );
9720
9721 if ( is_object( $fs ) ) {
9722 $fs->remove_sdk_reference();
9723
9724 self::require_plugin_essentials();
9725
9726 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9727 is_plugin_active( $fs->premium_plugin_basename() )
9728 ) {
9729 // Deleting Free or Premium plugin version while the other version still installed.
9730 return;
9731 }
9732
9733 if (
9734 ! $fs->is_clone() &&
9735 /**
9736 * If there's a context install, run this method only when there's also a context user (e.g., when cloning a subsite of a multisite network into a single-site installation, it's possible for an install to be associated with a non-existing user entity; we want Freemius to be off in this case, while we are trying to recover the user).
9737 *
9738 * @author Leo Fajardo
9739 */
9740 ( ! is_object( $fs->_site ) || $fs->is_registered() )
9741 ) {
9742 $fs->_uninstall_plugin_event();
9743 }
9744
9745 $fs->do_action( 'after_uninstall' );
9746 }
9747 }
9748
9749 #----------------------------------------------------------------------------------
9750 #region Plugin Information
9751 #----------------------------------------------------------------------------------
9752
9753 /**
9754 * Load WordPress core plugin.php essential module.
9755 *
9756 * @author Vova Feldman (@svovaf)
9757 * @since 1.1.1
9758 */
9759 private static function require_plugin_essentials() {
9760 if ( ! function_exists( 'get_plugins' ) ) {
9761 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9762
9763 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9764 }
9765 }
9766
9767 /**
9768 * Load WordPress core pluggable.php module.
9769 *
9770 * @author Vova Feldman (@svovaf)
9771 * @since 1.1.2
9772 */
9773 private static function require_pluggable_essentials() {
9774 if ( ! function_exists( 'wp_get_current_user' ) ) {
9775 require_once ABSPATH . 'wp-includes/pluggable.php';
9776 }
9777 }
9778
9779 /**
9780 * Return plugin data.
9781 *
9782 * @author Vova Feldman (@svovaf)
9783 * @since 1.0.1
9784 *
9785 * @param bool $reparse_plugin_metadata
9786 *
9787 * @return array
9788 */
9789 function get_plugin_data( $reparse_plugin_metadata = false ) {
9790 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9791 self::require_plugin_essentials();
9792
9793 if ( $this->is_plugin() ) {
9794 /**
9795 * @author Vova Feldman (@svovaf)
9796 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9797 *
9798 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9799 */
9800 $plugin_data = get_plugin_data(
9801 $this->_plugin_main_file_path,
9802 false,
9803 false
9804 );
9805 } else {
9806 $theme_data = wp_get_theme();
9807
9808 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9809 $parent_theme = $theme_data->parent();
9810
9811 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9812 $theme_data = $parent_theme;
9813 }
9814 }
9815
9816 $plugin_data = array(
9817 'Name' => $theme_data->get( 'Name' ),
9818 'Version' => $theme_data->get( 'Version' ),
9819 'Author' => $theme_data->get( 'Author' ),
9820 'Description' => $theme_data->get( 'Description' ),
9821 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9822 );
9823 }
9824
9825 $this->_plugin_data = $plugin_data;
9826 }
9827
9828 return $this->_plugin_data;
9829 }
9830
9831 /**
9832 * @author Vova Feldman (@svovaf)
9833 * @since 1.0.1
9834 * @since 1.2.2.5 If slug not set load slug by module ID.
9835 *
9836 * @return string Plugin slug.
9837 */
9838 function get_slug() {
9839 if ( ! isset( $this->_slug ) ) {
9840 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9841 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9842 }
9843
9844 return $this->_slug;
9845 }
9846
9847 /**
9848 * @author Leo Fajardo (@leorw)
9849 * @since 2.2.1
9850 *
9851 * @return string
9852 */
9853 function get_premium_slug() {
9854 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
9855 $this->_plugin->premium_slug :
9856 "{$this->_slug}-premium";
9857 }
9858
9859 /**
9860 * Retrieve the desired folder name for the product.
9861 *
9862 * @author Vova Feldman (@svovaf)
9863 * @since 1.2.1.7
9864 *
9865 * @return string Plugin slug.
9866 */
9867 function get_target_folder_name() {
9868 return $this->can_use_premium_code() ?
9869 $this->_plugin->premium_slug :
9870 $this->_slug;
9871 }
9872
9873 /**
9874 * @author Vova Feldman (@svovaf)
9875 * @since 1.0.1
9876 *
9877 * @return number Plugin ID.
9878 */
9879 function get_id() {
9880 return $this->_plugin->id;
9881 }
9882
9883 /**
9884 * @author Leo Fajardo (@leorw)
9885 * @since 2.2.4
9886 *
9887 * @return number|null Bundle ID.
9888 */
9889 function get_bundle_id() {
9890 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
9891 $this->_plugin->bundle_id :
9892 null;
9893 }
9894
9895 /**
9896 * @author Vova Feldman (@svovaf)
9897 * @since 2.3.1
9898 *
9899 * @return string|null Bundle public key.
9900 */
9901 function get_bundle_public_key() {
9902 return isset( $this->_plugin->bundle_public_key ) ?
9903 $this->_plugin->bundle_public_key :
9904 null;
9905 }
9906
9907 /**
9908 * Get whether the SDK has been initiated in the context of a Bundle.
9909 *
9910 * This will return true, if `bundle_id` is present in the SDK init parameters.
9911 *
9912 * ```php
9913 * $my_fs = fs_dynamic_init( array(
9914 * // ...
9915 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
9916 * 'bundle_public_key' => 'pk_XXXX',
9917 * ) );
9918 * ```
9919 *
9920 * @author Swashata Ghosh (@swashata)
9921 * @since 2.5.0
9922 *
9923 * @return bool True if we are running in bundle context, false otherwise.
9924 */
9925 private function has_bundle_context() {
9926 return ! is_null( $this->get_bundle_id() );
9927 }
9928
9929 /**
9930 * @author Vova Feldman (@svovaf)
9931 * @since 1.2.1.5
9932 *
9933 * @return string Freemius SDK version
9934 */
9935 function get_sdk_version() {
9936 return $this->version;
9937 }
9938
9939 /**
9940 * @author Vova Feldman (@svovaf)
9941 * @since 1.2.1.5
9942 *
9943 * @return number Parent plugin ID (if parent exist).
9944 */
9945 function get_parent_id() {
9946 return $this->is_addon() ?
9947 $this->get_parent_instance()->get_id() :
9948 $this->_plugin->id;
9949 }
9950
9951 /**
9952 * @author Vova Feldman (@svovaf)
9953 * @since 2.3.1
9954 *
9955 * @return string
9956 */
9957 function get_usage_tracking_terms_url() {
9958 return $this->apply_filters(
9959 'usage_tracking_terms_url',
9960 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
9961 );
9962 }
9963
9964 /**
9965 * @todo (For LiteSDK) We can refactor this and other related functions giving links to several landing pages on freemius.com to come from a separate class like `FS_Terms_Pages`. This would get a `FS_WP_Hook` (hypothetical) instance as a dependency and use it to hook into the `license_activation_terms_url` or related filters. The entry level instance from `ms_fs()` would hold a public read-only variable `my_fs()->terms_pages` which would be an instance of `FS_Terms_Pages` and would hold all the links to the terms pages.
9966 * @since 2.5.8
9967 *
9968 * @return string
9969 */
9970 function get_license_activation_terms_url() {
9971 return $this->apply_filters(
9972 'license_activation_terms_url',
9973 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
9974 );
9975 }
9976
9977 /**
9978 * @author Vova Feldman (@svovaf)
9979 * @since 2.3.1
9980 *
9981 * @return string
9982 */
9983 function get_eula_url() {
9984 return $this->apply_filters(
9985 'eula_url',
9986 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
9987 );
9988 }
9989
9990 /**
9991 * @author Vova Feldman (@svovaf)
9992 * @since 1.0.1
9993 *
9994 * @return string Plugin public key.
9995 */
9996 function get_public_key() {
9997 return $this->_plugin->public_key;
9998 }
9999
10000 /**
10001 * Will be available only on sandbox mode.
10002 *
10003 * @author Vova Feldman (@svovaf)
10004 * @since 1.0.4
10005 *
10006 * @return mixed Plugin secret key.
10007 */
10008 function get_secret_key() {
10009 return $this->_plugin->secret_key;
10010 }
10011
10012 /**
10013 * @author Vova Feldman (@svovaf)
10014 * @since 1.1.1
10015 *
10016 * @return bool
10017 */
10018 function has_secret_key() {
10019 return ! empty( $this->_plugin->secret_key );
10020 }
10021
10022 /**
10023 * @author Vova Feldman (@svovaf)
10024 * @since 1.0.9
10025 *
10026 * @param string|bool $premium_suffix
10027 *
10028 * @return string
10029 */
10030 function get_plugin_name( $premium_suffix = false ) {
10031 $this->_logger->entrance();
10032
10033 /**
10034 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10035 *
10036 * @author Vova Feldman
10037 */
10038 if ( ! isset( $this->_plugin_name ) ) {
10039 // Name is not yet set.
10040 $this->set_name( $premium_suffix );
10041 } else if (
10042 ! empty( $premium_suffix ) &&
10043 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10044 ) {
10045 // Name is already set, but there's a change in the premium suffix.
10046 $this->set_name( $premium_suffix );
10047 }
10048
10049 return $this->_plugin_name;
10050 }
10051
10052 /**
10053 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10054 *
10055 * @author Vova Feldman (@svovaf)
10056 * @since 2.2.1
10057 *
10058 * @param string $premium_suffix
10059 */
10060 private function set_name( $premium_suffix = '' ) {
10061 $plugin_data = $this->get_plugin_data();
10062
10063 // Get name.
10064 $this->_plugin_name = $plugin_data['Name'];
10065
10066 if ( is_string( $premium_suffix ) ) {
10067 $premium_suffix = trim( $premium_suffix );
10068
10069 if ( ! empty( $premium_suffix ) ) {
10070 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10071 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10072 $suffix_len = strlen( $suffix );
10073
10074 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10075 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10076 ) {
10077 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10078 }
10079 }
10080 }
10081
10082 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10083 }
10084
10085 /**
10086 * @author Vova Feldman (@svovaf)
10087 * @since 1.0.0
10088 *
10089 * @param bool $reparse_plugin_metadata
10090 *
10091 * @return string
10092 */
10093 function get_plugin_version( $reparse_plugin_metadata = false ) {
10094 $this->_logger->entrance();
10095
10096 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10097
10098 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10099
10100 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10101 }
10102
10103 /**
10104 * @author Vova Feldman (@svovaf)
10105 * @since 1.2.1.7
10106 *
10107 * @return string
10108 */
10109 function get_plugin_title() {
10110 $this->_logger->entrance();
10111
10112 $title = $this->_plugin->title;
10113
10114 return $this->apply_filters( 'plugin_title', $title );
10115 }
10116
10117 /**
10118 * @author Vova Feldman (@svovaf)
10119 * @since 1.2.2.7
10120 *
10121 * @param bool $lowercase
10122 *
10123 * @return string
10124 */
10125 function get_module_label( $lowercase = false ) {
10126 $label = $this->is_addon() ?
10127 $this->get_text_inline( 'Add-On', 'addon' ) :
10128 ( $this->is_plugin() ?
10129 $this->get_text_inline( 'Plugin', 'plugin' ) :
10130 $this->get_text_inline( 'Theme', 'theme' ) );
10131
10132 if ( $lowercase ) {
10133 $label = strtolower( $label );
10134 }
10135
10136 return $label;
10137 }
10138
10139 /**
10140 * @author Vova Feldman (@svovaf)
10141 * @since 1.0.4
10142 *
10143 * @return string
10144 */
10145 function get_plugin_basename() {
10146 if ( ! isset( $this->_plugin_basename ) ) {
10147 if ( $this->is_plugin() ) {
10148 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10149 } else {
10150 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10151 }
10152 }
10153
10154 return $this->_plugin_basename;
10155 }
10156
10157 function get_plugin_folder_name() {
10158 $this->_logger->entrance();
10159
10160 $plugin_folder = $this->_plugin_basename;
10161
10162 while ( '.' !== dirname( $plugin_folder ) ) {
10163 $plugin_folder = dirname( $plugin_folder );
10164 }
10165
10166 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10167
10168 return $plugin_folder;
10169 }
10170
10171 #endregion ------------------------------------------------------------------
10172
10173 /* Account
10174 ------------------------------------------------------------------------------------------------------------------*/
10175
10176 /**
10177 * Find plugin's slug by plugin's basename.
10178 *
10179 * @author Vova Feldman (@svovaf)
10180 * @since 1.0.9
10181 *
10182 * @param string $plugin_base_name
10183 *
10184 * @return false|string
10185 */
10186 private static function find_slug_by_basename( $plugin_base_name ) {
10187 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10188
10189 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10190 return false;
10191 }
10192
10193 return $file_slug_map[ $plugin_base_name ];
10194 }
10195
10196 /**
10197 * Store the map between the plugin's basename to the slug.
10198 *
10199 * @author Vova Feldman (@svovaf)
10200 * @since 1.0.9
10201 */
10202 private function store_file_slug_map() {
10203 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10204
10205 if ( ! array( $file_slug_map ) ) {
10206 $file_slug_map = array();
10207 }
10208
10209 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10210 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10211 ) {
10212 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10213 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10214 }
10215 }
10216
10217 /**
10218 * @return array[number]FS_User
10219 */
10220 static function get_all_users() {
10221 $users = self::maybe_get_entities_account_option( 'users', array() );
10222
10223 if ( ! is_array( $users ) ) {
10224 $users = array();
10225 }
10226
10227 return $users;
10228 }
10229
10230 /**
10231 * @param string $module_type
10232 * @param null|int $blog_id Since 2.0.0
10233 *
10234 * @return array[string]FS_Site
10235 */
10236 public static function get_all_sites(
10237 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10238 $blog_id = null,
10239 $is_backup = false
10240 ) {
10241 $sites = self::get_account_option(
10242 ( $is_backup ? 'prev_' : '' ) . 'sites',
10243 $module_type,
10244 $blog_id
10245 );
10246
10247 if ( ! is_array( $sites ) ) {
10248 $sites = array();
10249 }
10250
10251 return $sites;
10252 }
10253
10254 /**
10255 * @author Leo Fajardo (@leorw)
10256 *
10257 * @since 1.2.2
10258 *
10259 * @param string $option_name
10260 * @param string $module_type
10261 * @param null|int $network_level_or_blog_id Since 2.0.0
10262 *
10263 * @return mixed
10264 */
10265 public static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10266 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10267 $option_name = $module_type . '_' . $option_name;
10268 }
10269
10270 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10271 }
10272
10273 /**
10274 * @author Leo Fajardo (@leorw)
10275 *
10276 * @since 1.2.2
10277 *
10278 * @param string $option_name
10279 * @param mixed $option_value
10280 * @param bool $store
10281 * @param null|int $network_level_or_blog_id Since 2.0.0
10282 */
10283 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10284 self::set_account_option_by_module(
10285 $this->_module_type,
10286 $option_name,
10287 $option_value,
10288 $store,
10289 $network_level_or_blog_id
10290 );
10291 }
10292
10293 /**
10294 * @author Vova Feldman (@svovaf)
10295 *
10296 * @since 1.2.2.7
10297 *
10298 * @param string $module_type
10299 * @param string $option_name
10300 * @param mixed $option_value
10301 * @param bool $store
10302 * @param null|int $network_level_or_blog_id Since 2.0.0
10303 */
10304 private static function set_account_option_by_module(
10305 $module_type,
10306 $option_name,
10307 $option_value,
10308 $store,
10309 $network_level_or_blog_id = null
10310 ) {
10311 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10312 $option_name = $module_type . '_' . $option_name;
10313 }
10314
10315 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10316 }
10317
10318 /**
10319 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10320 *
10321 * @author Leo Fajardo (@leorw)
10322 * @since 2.3.1
10323 *
10324 * @param string $option_name
10325 * @param mixed $default
10326 * @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).
10327 *
10328 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10329 */
10330 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10331 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10332
10333 $class_name = '';
10334
10335 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10336 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10337 }
10338
10339 switch ( $option_name ) {
10340 case 'plugins':
10341 case 'themes':
10342 case 'addons':
10343 $class_name = FS_Plugin::get_class_name();
10344 break;
10345 case 'users':
10346 $class_name = FS_User::get_class_name();
10347 break;
10348 case 'sites':
10349 $class_name = FS_Site::get_class_name();
10350 break;
10351 case 'licenses':
10352 case 'all_licenses':
10353 $class_name = FS_Plugin_License::get_class_name();
10354 break;
10355 case 'plans':
10356 $class_name = FS_Plugin_Plan::get_class_name();
10357 break;
10358 case 'updates':
10359 $class_name = FS_Plugin_Tag::get_class_name();
10360 break;
10361 }
10362
10363 if ( empty( $class_name ) ) {
10364 return $option;
10365 }
10366
10367 return fs_get_entities( $option, $class_name );
10368 }
10369
10370 /**
10371 * @author Vova Feldman (@svovaf)
10372 * @since 1.0.6
10373 *
10374 * @param number|null $module_id
10375 *
10376 * @return FS_Plugin_License[]
10377 */
10378 private static function get_all_licenses( $module_id = null ) {
10379 $licenses = self::get_account_option( 'all_licenses' );
10380
10381 if ( ! is_array( $licenses ) ) {
10382 $licenses = array();
10383 }
10384
10385 if ( is_null( $module_id ) ) {
10386 return $licenses;
10387 }
10388
10389 $licenses = isset( $licenses[ $module_id ] ) ?
10390 $licenses[ $module_id ] :
10391 array();
10392
10393 return $licenses;
10394 }
10395
10396 /**
10397 * @author Leo Fajardo (@leorw)
10398 * @since 2.0.0
10399 *
10400 * @param number $module_id
10401 * @param number|null $user_id
10402 *
10403 * @return array
10404 */
10405 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10406 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10407
10408 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10409 $all_modules_user_id_license_ids_map = array();
10410 }
10411
10412 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10413 $all_modules_user_id_license_ids_map[ $module_id ] :
10414 array();
10415
10416 if ( FS_User::is_valid_id( $user_id ) ) {
10417 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10418 $user_id_license_ids_map[ $user_id ] :
10419 array();
10420 }
10421
10422 return $user_id_license_ids_map;
10423 }
10424
10425 /**
10426 * @author Leo Fajardo (@leorw)
10427 * @since 2.0.0
10428 *
10429 * @param array $new_user_id_license_ids_map
10430 * @param number $module_id
10431 * @param number|null $user_id
10432 */
10433 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10434 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10435 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10436 $all_modules_user_id_license_ids_map = array();
10437 }
10438
10439 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10440 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10441 }
10442
10443 if ( FS_User::is_valid_id( $user_id ) ) {
10444 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10445 } else {
10446 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10447 }
10448
10449 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10450 }
10451
10452 /**
10453 * Get a collection of the user's linked license IDs.
10454 *
10455 * @author Vova Feldman (@svovaf)
10456 * @since 2.0.0
10457 *
10458 * @param number $user_id
10459 *
10460 * @return number[]
10461 */
10462 private function get_user_linked_license_ids( $user_id ) {
10463 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10464 }
10465
10466 /**
10467 * Override the user's linked license IDs with a new IDs collection.
10468 *
10469 * @author Vova Feldman (@svovaf)
10470 * @since 2.0.0
10471 *
10472 * @param number $user_id
10473 * @param number[] $license_ids
10474 */
10475 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10476 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10477 }
10478
10479 /**
10480 * Link a specified license ID to a given user.
10481 *
10482 * @author Vova Feldman (@svovaf)
10483 * @since 2.0.0
10484 *
10485 * @param number $license_id
10486 * @param number $user_id
10487 */
10488 private function link_license_2_user( $license_id, $user_id ) {
10489 $license_ids = $this->get_user_linked_license_ids( $user_id );
10490
10491 if ( in_array( $license_id, $license_ids ) ) {
10492 // License already linked.
10493 return;
10494 }
10495
10496 $license_ids[] = $license_id;
10497
10498 $this->set_user_linked_license_ids( $user_id, $license_ids );
10499 }
10500
10501 /**
10502 * @param string|bool $module_type
10503 *
10504 * @return FS_Plugin_Plan[]
10505 */
10506 private static function get_all_plans( $module_type = false ) {
10507 $plans = self::get_account_option( 'plans', $module_type );
10508
10509 if ( ! is_array( $plans ) ) {
10510 $plans = array();
10511 }
10512
10513 return $plans;
10514 }
10515
10516 /**
10517 * @author Vova Feldman (@svovaf)
10518 * @since 1.0.4
10519 *
10520 * @return FS_Plugin_Tag[]
10521 */
10522 private static function get_all_updates() {
10523 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10524
10525 if ( ! is_array( $updates ) ) {
10526 $updates = array();
10527 }
10528
10529 return $updates;
10530 }
10531
10532 /**
10533 * @author Vova Feldman (@svovaf)
10534 * @since 1.0.6
10535 *
10536 * @return array<number,FS_Plugin[]>|false
10537 */
10538 public static function get_all_addons() {
10539 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10540
10541 if ( ! is_array( $addons ) ) {
10542 $addons = array();
10543 }
10544
10545 return $addons;
10546 }
10547
10548 /**
10549 * @author Vova Feldman (@svovaf)
10550 * @since 1.0.6
10551 *
10552 * @return number[]|false
10553 */
10554 public static function get_all_account_addons() {
10555 $addons = self::$_accounts->get_option( 'account_addons', array() );
10556
10557 if ( ! is_array( $addons ) ) {
10558 $addons = array();
10559 }
10560
10561 return $addons;
10562 }
10563
10564 /**
10565 * Check if user has connected his account (opted-in).
10566 *
10567 * Note:
10568 * If the user opted-in and opted-out on a later stage,
10569 * this will still return true. If you want to check if the
10570 * user is currently opted-in, use:
10571 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10572 *
10573 * @author Vova Feldman (@svovaf)
10574 * @since 1.0.1
10575 *
10576 * @param bool $ignore_anonymous_state Since 2.5.1
10577 *
10578 * @return bool
10579 */
10580 function is_registered( $ignore_anonymous_state = false ) {
10581 return (
10582 is_object( $this->_user ) &&
10583 (
10584 $this->is_premium() ||
10585 $ignore_anonymous_state ||
10586 ! $this->is_anonymous()
10587 )
10588 );
10589 }
10590
10591 /**
10592 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10593 *
10594 * @author Leo Fajardo (@leorw)
10595 * @since 1.2.1.5
10596 *
10597 * @return bool
10598 */
10599 function is_tracking_allowed( $blog_id = null, $install = null ) {
10600 if ( is_null( $install ) ) {
10601 $install = is_null( $blog_id ) ?
10602 $this->_site :
10603 $this->get_install_by_blog_id( $blog_id );
10604 }
10605
10606 return (
10607 is_object( $install ) &&
10608 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
10609 );
10610 }
10611
10612 /**
10613 * Returns TRUE if the user never opted-in or manually opted-out.
10614 *
10615 * @author Vova Feldman (@svovaf)
10616 * @since 1.2.1.5
10617 *
10618 * @param int|null $blog_id
10619 *
10620 * @return bool
10621 */
10622 function is_tracking_prohibited( $blog_id = null ) {
10623 return (
10624 ! $this->is_registered( true ) ||
10625 ! $this->is_tracking_allowed( $blog_id )
10626 );
10627 }
10628
10629 /**
10630 * @author Leo Fajardo (@leorw)
10631 * @since 2.4.0
10632 *
10633 * @return bool
10634 */
10635 function is_bundle_license_auto_activation_enabled() {
10636 return $this->is_addon() ?
10637 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10638 $this->_is_bundle_license_auto_activation_enabled;
10639 }
10640
10641 /**
10642 * @author Vova Feldman (@svovaf)
10643 * @since 1.0.4
10644 *
10645 * @return FS_Plugin
10646 */
10647 function get_plugin() {
10648 return $this->_plugin;
10649 }
10650
10651 /**
10652 * @author Vova Feldman (@svovaf)
10653 * @since 1.0.3
10654 *
10655 * @return FS_User
10656 */
10657 function get_user() {
10658 return $this->_user;
10659 }
10660
10661 /**
10662 * @author Vova Feldman (@svovaf)
10663 * @since 1.0.3
10664 *
10665 * @return FS_Site
10666 */
10667 function get_site() {
10668 return $this->_site;
10669 }
10670
10671 /**
10672 * @author Daniele Alessandra (@danielealessandra)
10673 * @return FS_Storage
10674 * @since 2.6.2
10675 *
10676 */
10677 public function get_storage() {
10678 return $this->_storage;
10679 }
10680
10681 /**
10682 * @author Leo Fajardo (@leorw)
10683 * @since 2.5.0
10684 */
10685 function store_site( $site ) {
10686 $this->_site = $site;
10687 $this->_store_site( true );
10688 }
10689
10690 /**
10691 * Deletes the current install with an option to back it up in case restoration will be needed (e.g., if the automatic clone resolution attempt fails).
10692 *
10693 * @author Leo Fajardo (@leorw)
10694 * @since 2.5.0
10695 */
10696 function delete_current_install( $back_up ) {
10697 // Back up and delete the unique ID.
10698 if ( $back_up ) {
10699 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
10700 }
10701
10702 self::$_accounts->set_option( 'unique_id', null );
10703
10704 if ( $back_up ) {
10705 // Back up the install before deleting it so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails).
10706 $this->back_up_site();
10707 }
10708
10709 $this->_delete_site();
10710 $this->_site = null;
10711 }
10712
10713 /**
10714 * @author Leo Fajardo (@leorw)
10715 * @since 2.5.0
10716 */
10717 function restore_backup_site() {
10718 self::$_accounts->set_option(
10719 'unique_id',
10720 self::$_accounts->get_option( 'prev_unique_id' )
10721 );
10722
10723 $sites = self::get_all_sites( $this->_module_type, null, true );
10724 $this->store_site( clone $sites[ $this->_slug ] );
10725 }
10726
10727 /**
10728 * Get plugin add-ons.
10729 *
10730 * @author Vova Feldman (@svovaf)
10731 * @since 1.0.6
10732 *
10733 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10734 *
10735 * @param bool $flush
10736 *
10737 * @return FS_Plugin[]|false
10738 */
10739 function get_addons( $flush = false ) {
10740 $this->_logger->entrance();
10741
10742 if ( ! $this->_has_addons ) {
10743 return false;
10744 }
10745
10746 $addons = $this->sync_addons( $flush );
10747
10748 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10749 false :
10750 $addons;
10751 }
10752
10753 /**
10754 * @author Vova Feldman (@svovaf)
10755 * @since 1.0.6
10756 *
10757 * @return number[]|false
10758 */
10759 function get_account_addons() {
10760 $this->_logger->entrance();
10761
10762 $addons = self::get_all_account_addons();
10763
10764 if ( ! is_array( $addons ) ||
10765 ! isset( $addons[ $this->_plugin->id ] ) ||
10766 ! is_array( $addons[ $this->_plugin->id ] ) ||
10767 0 === count( $addons[ $this->_plugin->id ] )
10768 ) {
10769 return false;
10770 }
10771
10772 return $addons[ $this->_plugin->id ];
10773 }
10774
10775 /**
10776 * Check if user has any
10777 *
10778 * @author Vova Feldman (@svovaf)
10779 * @since 1.1.6
10780 *
10781 * @return bool
10782 */
10783 function has_account_addons() {
10784 $addons = $this->get_account_addons();
10785
10786 return is_array( $addons ) && ( 0 < count( $addons ) );
10787 }
10788
10789
10790 /**
10791 * Get add-on by ID (from local data).
10792 *
10793 * @author Vova Feldman (@svovaf)
10794 * @since 1.0.6
10795 *
10796 * @param number $id
10797 *
10798 * @return FS_Plugin|false
10799 */
10800 function get_addon( $id ) {
10801 $this->_logger->entrance();
10802
10803 $addons = $this->get_addons();
10804
10805 if ( is_array( $addons ) ) {
10806 foreach ( $addons as $addon ) {
10807 if ( $id == $addon->id ) {
10808 return $addon;
10809 }
10810 }
10811 }
10812
10813 return false;
10814 }
10815
10816 /**
10817 * Get add-on by slug (from local data).
10818 *
10819 * @author Vova Feldman (@svovaf)
10820 * @since 1.0.6
10821 *
10822 * @param string $slug
10823 *
10824 * @param bool $flush
10825 *
10826 * @return FS_Plugin|false
10827 */
10828 function get_addon_by_slug( $slug, $flush = false ) {
10829 $this->_logger->entrance();
10830
10831 $addons = $this->get_addons( $flush );
10832
10833 if ( is_array( $addons ) ) {
10834 foreach ( $addons as $addon ) {
10835 if ( $slug === $addon->slug ) {
10836 return $addon;
10837 }
10838 }
10839 }
10840
10841 return false;
10842 }
10843
10844 /**
10845 * @var array<number,object[]> {
10846 * @key number Add-on ID.
10847 * @val object[] The add-on's plans and prices object.
10848 * }
10849 */
10850 private $plans_and_pricing_by_addon_id;
10851
10852 /**
10853 * @author Leo Fajardo (@leorw)
10854 * @since 2.3.0
10855 *
10856 * @return array<number,object[]> {
10857 * @key number Add-on ID.
10858 * @val object[] The add-on's plans and prices object.
10859 * }
10860 */
10861 function _get_addons_plans_and_pricing_map_by_id() {
10862 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
10863 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
10864
10865 $plans_and_pricing_by_addon_id = array();
10866 if ( $this->is_api_result_object( $result, 'addons' ) ) {
10867 foreach ( $result->addons as $addon ) {
10868 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
10869 }
10870 }
10871
10872 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
10873 }
10874
10875 return $this->plans_and_pricing_by_addon_id;
10876 }
10877
10878 /**
10879 * @author Leo Fajardo (@leorw)
10880 * @since 2.3.0
10881 *
10882 * @param number $addon_id
10883 * @param bool $is_installed
10884 *
10885 * @return array
10886 */
10887 function _get_addon_info( $addon_id, $is_installed ) {
10888 $addon = $this->get_addon( $addon_id );
10889
10890 if ( ! is_object( $addon ) ) {
10891 // Unexpected call.
10892 return array();
10893 }
10894
10895 $slug = $addon->slug;
10896
10897 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
10898
10899 if ( ! fs_is_network_admin() ) {
10900 // Get blog-level activated installations.
10901 $sites = self::maybe_get_entities_account_option( 'sites', array() );
10902 } else {
10903 $sites = null;
10904
10905 if ( $this->is_addon_activated( $addon_id ) &&
10906 $this->get_addon_instance( $addon_id )->is_network_active()
10907 ) {
10908 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
10909 // Get network-level activated installations.
10910 $sites = self::maybe_get_entities_account_option(
10911 'sites',
10912 array(),
10913 $addon_storage->network_install_blog_id
10914 );
10915 }
10916 }
10917 }
10918
10919 $addon_info = array(
10920 'is_connected' => false,
10921 'slug' => $slug,
10922 'title' => $addon->title,
10923 'is_whitelabeled' => $addon_storage->is_whitelabeled
10924 );
10925
10926 if ( ! $is_installed ) {
10927 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
10928
10929 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
10930 $has_paid_plan = false;
10931 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
10932
10933 if ( is_array( $plans ) && count( $plans ) > 0 ) {
10934 foreach ( $plans as $plan ) {
10935 if ( isset( $plan->pricing ) &&
10936 is_array( $plan->pricing ) &&
10937 count( $plan->pricing ) > 0
10938 ) {
10939 $has_paid_plan = true;
10940 break;
10941 }
10942 }
10943 }
10944
10945 $addon_info['has_paid_plan'] = $has_paid_plan;
10946 }
10947 }
10948
10949 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
10950 return $addon_info;
10951 }
10952
10953 $site = $sites[ $slug ];
10954
10955 $addon_info['is_connected'] = (
10956 ( $addon->parent_plugin_id == $this->get_id() ) &&
10957 is_object( $site ) &&
10958 FS_Site::is_valid_id( $site->id ) &&
10959 FS_User::is_valid_id( $site->user_id ) &&
10960 FS_Plugin_Plan::is_valid_id( $site->plan_id )
10961 );
10962
10963 if ( $addon_info['is_connected'] && $is_installed ) {
10964 return $addon_info;
10965 }
10966
10967 $addon_info['site'] = $site;
10968
10969 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
10970 if ( isset( $plugins_data[ $slug ] ) ) {
10971 $plugin_data = $plugins_data[ $slug ];
10972
10973 $addon_info['version'] = $plugin_data->version;
10974 }
10975
10976 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
10977 if ( isset( $all_plans[ $slug ] ) ) {
10978 $plans = $all_plans[ $slug ];
10979
10980 foreach ( $plans as $plan ) {
10981 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
10982 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
10983 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
10984 break;
10985 }
10986 }
10987 }
10988
10989 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
10990 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
10991 foreach ( $licenses[ $addon_id ] as $license ) {
10992 if ( $license->id == $site->license_id ) {
10993 $addon_info['license'] = $license;
10994 break;
10995 }
10996 }
10997 }
10998
10999 if ( isset( $addon_info['license'] ) ) {
11000 if ( isset( $addon_storage->subscriptions ) &&
11001 ! empty( $addon_storage->subscriptions )
11002 ) {
11003 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11004
11005 foreach ( $addon_subscriptions as $subscription ) {
11006 if ( $subscription->license_id == $site->license_id ) {
11007 $addon_info['subscription'] = $subscription;
11008 break;
11009 }
11010 }
11011 }
11012 }
11013
11014 return $addon_info;
11015 }
11016
11017 /**
11018 * @author Vova Feldman (@svovaf)
11019 * @since 2.0.0
11020 *
11021 * @param number $user_id
11022 *
11023 * @return FS_User
11024 */
11025 static function _get_user_by_id( $user_id ) {
11026 self::$_static_logger->entrance( "user_id = {$user_id}" );
11027
11028 $users = self::get_all_users();
11029
11030 if ( is_array( $users ) ) {
11031 if ( isset( $users[ $user_id ] ) &&
11032 $users[ $user_id ] instanceof FS_User &&
11033 $user_id == $users[ $user_id ]->id
11034 ) {
11035 return $users[ $user_id ];
11036 }
11037
11038 // If user wasn't found by the key, iterate over all the users collection.
11039 foreach ( $users as $user ) {
11040 /**
11041 * @var FS_User $user
11042 */
11043 if ( $user_id == $user->id ) {
11044 return $user;
11045 }
11046 }
11047 }
11048
11049 return null;
11050 }
11051
11052 /**
11053 * Checks if a Freemius user_id is associated with a super-admin.
11054 *
11055 * @author Vova Feldman (@svovaf)
11056 * @since 2.0.0
11057 *
11058 * @param number $user_id
11059 *
11060 * @return bool
11061 */
11062 private static function is_super_admin( $user_id ) {
11063 $is_super_admin = false;
11064
11065 $user = self::_get_user_by_id( $user_id );
11066
11067 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11068 self::require_pluggable_essentials();
11069
11070 $wp_user = get_user_by( 'email', $user->email );
11071
11072 if ( $wp_user instanceof WP_User ) {
11073 $super_admins = get_super_admins();
11074 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11075 }
11076 }
11077
11078 return $is_super_admin;
11079 }
11080
11081 #----------------------------------------------------------------------------------
11082 #region Plans & Licensing
11083 #----------------------------------------------------------------------------------
11084
11085 /**
11086 * Check if running premium plugin code.
11087 *
11088 * @author Vova Feldman (@svovaf)
11089 * @since 1.0.5
11090 *
11091 * @return bool
11092 */
11093 function is_premium() {
11094 /**
11095 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11096 * `register_constructor_hooks` method.
11097 *
11098 * @author Leo Fajardo (@leorw)
11099 * @since 2.2.3
11100 */
11101 return is_object( $this->_plugin ) ?
11102 $this->_plugin->is_premium :
11103 false;
11104 }
11105
11106 /**
11107 * Get site's plan ID.
11108 *
11109 * @author Vova Feldman (@svovaf)
11110 * @since 1.0.2
11111 *
11112 * @return number
11113 */
11114 function get_plan_id() {
11115 return $this->_site->plan_id;
11116 }
11117
11118 /**
11119 * Get site's plan title.
11120 *
11121 * @author Vova Feldman (@svovaf)
11122 * @since 1.0.2
11123 *
11124 * @return string
11125 */
11126 function get_plan_title() {
11127 $plan = $this->get_plan();
11128
11129 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11130 }
11131
11132 /**
11133 * Get site's plan name.
11134 *
11135 * @author Vova Feldman (@svovaf)
11136 * @since 2.0.0
11137 *
11138 * @return string
11139 */
11140 function get_plan_name() {
11141 $plan = $this->get_plan();
11142
11143 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11144 }
11145
11146 /**
11147 * @author Vova Feldman (@svovaf)
11148 * @since 1.0.9
11149 *
11150 * @return FS_Plugin_Plan|false
11151 */
11152 function get_plan() {
11153 if ( ! is_object( $this->_site ) ) {
11154 return false;
11155 }
11156
11157 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11158 $this->_get_plan_by_id( $this->_site->plan_id ) :
11159 false;
11160 }
11161
11162 /**
11163 * @author Vova Feldman (@svovaf)
11164 * @since 1.0.3
11165 *
11166 * @return bool
11167 */
11168 function is_trial() {
11169 $this->_logger->entrance();
11170
11171 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11172 return false;
11173 }
11174
11175 return $this->_site->is_trial();
11176 }
11177
11178 /**
11179 * Check if currently in a trial with payment method (credit card or paypal).
11180 *
11181 * @author Vova Feldman (@svovaf)
11182 * @since 1.1.7
11183 *
11184 * @return bool
11185 */
11186 function is_paid_trial() {
11187 $this->_logger->entrance();
11188
11189 if ( ! $this->is_trial() ) {
11190 return false;
11191 }
11192
11193 if ( ! $this->has_active_valid_license() ) {
11194 return false;
11195 }
11196
11197 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11198 return false;
11199 }
11200
11201 /**
11202 * @var FS_Subscription $subscription
11203 */
11204 $subscription = $this->_get_subscription( $this->_license->id );
11205
11206 return ( is_object( $subscription ) && $subscription->is_active() );
11207 }
11208
11209 /**
11210 * Check if trial already utilized.
11211 *
11212 * @since 1.0.9
11213 *
11214 * @return bool
11215 */
11216 function is_trial_utilized() {
11217 $this->_logger->entrance();
11218
11219 if ( ! $this->is_registered() ) {
11220 return false;
11221 }
11222
11223 return $this->_site->is_trial_utilized();
11224 }
11225
11226 /**
11227 * Get trial plan information (if in trial).
11228 *
11229 * @author Vova Feldman (@svovaf)
11230 * @since 1.0.9
11231 *
11232 * @return bool|FS_Plugin_Plan
11233 */
11234 function get_trial_plan() {
11235 $this->_logger->entrance();
11236
11237 if ( ! $this->is_trial() ) {
11238 return false;
11239 }
11240
11241 // Try to load plan from local cache.
11242 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11243
11244 if ( ! is_object( $trial_plan ) ) {
11245 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11246
11247 /**
11248 * If managed to fetch the plan, add it to the plans collection.
11249 */
11250 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11251 if ( ! is_array( $this->_plans ) ) {
11252 $this->_plans = array();
11253 }
11254
11255 $this->_plans[] = $trial_plan;
11256 $this->_store_plans();
11257 }
11258 }
11259
11260 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11261 return $trial_plan;
11262 }
11263
11264 /**
11265 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11266 */
11267 $trial_plan = new FS_Plugin_Plan();
11268 $trial_plan->id = $this->_site->trial_plan_id;
11269 $trial_plan->name = 'pro';
11270 $trial_plan->title = 'Pro';
11271
11272 return $trial_plan;
11273 }
11274
11275 /**
11276 * Check if the user has an activate, non-expired license on current plugin's install.
11277 *
11278 * @since 1.0.9
11279 *
11280 * @return bool
11281 */
11282 function is_paying() {
11283 $this->_logger->entrance();
11284
11285 if ( ! $this->is_registered( true ) ) {
11286 return false;
11287 }
11288
11289 if ( ! $this->has_paid_plan() ) {
11290 return false;
11291 }
11292
11293 return (
11294 ! $this->is_trial() &&
11295 'free' !== $this->get_plan_name() &&
11296 $this->has_active_valid_license()
11297 );
11298 }
11299
11300 /**
11301 * @author Vova Feldman (@svovaf)
11302 * @since 1.0.4
11303 *
11304 * @return bool
11305 */
11306 function is_free_plan() {
11307 if ( ! $this->is_registered() ) {
11308 return true;
11309 }
11310
11311 if ( ! $this->has_paid_plan() ) {
11312 return true;
11313 }
11314
11315 return (
11316 'free' === $this->get_plan_name() ||
11317 ! $this->has_features_enabled_license()
11318 );
11319 }
11320
11321 /**
11322 * @author Vova Feldman (@svovaf)
11323 * @since 1.0.5
11324 *
11325 * @return bool
11326 */
11327 function _has_premium_license() {
11328 $this->_logger->entrance();
11329
11330 $premium_license = $this->_get_available_premium_license();
11331
11332 return ( false !== $premium_license );
11333 }
11334
11335 /**
11336 * Check if user has any licenses associated with the plugin (including expired or blocking).
11337 *
11338 * @author Vova Feldman (@svovaf)
11339 * @since 1.1.7.3
11340 *
11341 * @param bool $including_foreign
11342 *
11343 * @return bool
11344 */
11345 function has_any_license( $including_foreign = true ) {
11346 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11347 return false;
11348 }
11349
11350 if ( $including_foreign ) {
11351 return true;
11352 }
11353
11354 foreach ( $this->_licenses as $license ) {
11355 if ( $this->_user->id == $license->user_id ) {
11356 return true;
11357 }
11358 }
11359
11360 return false;
11361 }
11362
11363 /**
11364 * @author Vova Feldman (@svovaf)
11365 * @since 1.0.5
11366 *
11367 * @param bool|null $is_localhost
11368 *
11369 * @return FS_Plugin_License|false
11370 */
11371 function _get_available_premium_license( $is_localhost = null ) {
11372 $this->_logger->entrance();
11373
11374 $licenses = $this->get_available_premium_licenses( $is_localhost );
11375 if ( ! empty( $licenses ) ) {
11376 return $licenses[0];
11377 }
11378
11379 return false;
11380 }
11381
11382 /**
11383 * @author Vova Feldman (@svovaf)
11384 * @since 1.0.5
11385 *
11386 * @param bool|null $is_localhost
11387 *
11388 * @return FS_Plugin_License[]
11389 */
11390 function get_available_premium_licenses( $is_localhost = null ) {
11391 $this->_logger->entrance();
11392
11393 $licenses = array();
11394 if ( ! $this->has_paid_plan() ) {
11395 return $licenses;
11396 }
11397
11398 if ( is_array( $this->_licenses ) ) {
11399 foreach ( $this->_licenses as $license ) {
11400 if ( ! $license->can_activate( $is_localhost ) ) {
11401 continue;
11402 }
11403
11404 $licenses[] = $license;
11405 }
11406 }
11407
11408 return $licenses;
11409 }
11410
11411 /**
11412 * Sync local plugin plans with remote server.
11413 *
11414 * 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).
11415 *
11416 * @author Vova Feldman (@svovaf)
11417 * @since 1.0.5
11418 *
11419 * @return FS_Plugin_Plan[]|object
11420 */
11421 function _sync_plans() {
11422 $plans = $this->_fetch_plugin_plans();
11423
11424 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11425 $plans_map = array();
11426 foreach ( $plans as $plan ) {
11427 $plans_map[ $plan->id ] = true;
11428 }
11429
11430 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11431
11432 foreach ( $plans_ids_to_keep as $plan_id ) {
11433 if ( isset( $plans_map[ $plan_id ] ) ) {
11434 continue;
11435 }
11436
11437 $missing_plan = self::_get_plan_by_id( $plan_id );
11438
11439 if ( is_object( $missing_plan ) ) {
11440 $plans[] = $missing_plan;
11441 }
11442 }
11443
11444 $this->_plans = $plans;
11445 $this->_store_plans();
11446 }
11447
11448 $this->do_action( 'after_plans_sync', $plans );
11449
11450 return $this->_plans;
11451 }
11452
11453 /**
11454 * Check if specified plan exists locally. If not, fetch it and store it.
11455 *
11456 * @author Vova Feldman (@svovaf)
11457 * @since 2.0.0
11458 *
11459 * @param number $plan_id
11460 *
11461 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11462 */
11463 private function sync_plan_if_not_exist( $plan_id ) {
11464 $plan = self::_get_plan_by_id( $plan_id );
11465
11466 if ( is_object( $plan ) ) {
11467 // Plan already exists.
11468 return $plan;
11469 }
11470
11471 $plan = $this->fetch_plan_by_id( $plan_id );
11472
11473 if ( $plan instanceof FS_Plugin_Plan ) {
11474 $this->_plans[] = $plan;
11475 $this->_store_plans();
11476
11477 return $plan;
11478 }
11479
11480 return $plan;
11481 }
11482
11483 /**
11484 * Check if specified license exists locally. If not, fetch it and store it.
11485 *
11486 * @author Vova Feldman (@svovaf)
11487 * @since 2.0.0
11488 *
11489 * @param number $license_id
11490 * @param string $license_key
11491 *
11492 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11493 */
11494 private function sync_license_if_not_exist( $license_id, $license_key ) {
11495 $license = $this->_get_license_by_id( $license_id );
11496
11497 if ( is_object( $license ) ) {
11498 // License already exists.
11499 return $license;
11500 }
11501
11502 $license = $this->fetch_license_by_key( $license_id, $license_key );
11503
11504 if ( $license instanceof FS_Plugin_License ) {
11505 $this->_licenses[] = $license;
11506
11507 $this->set_license( $license );
11508
11509 $this->_store_licenses();
11510
11511 return $license;
11512 }
11513
11514 return $license;
11515 }
11516
11517 /**
11518 * Get a collection of unique plan IDs that are associated with any installs in the network.
11519 *
11520 * @author Leo Fajardo (@leorw)
11521 * @since 2.0.0
11522 *
11523 * @return number[]
11524 */
11525 private function get_plans_ids_associated_with_installs() {
11526 if ( ! is_multisite() ) {
11527 if ( ! is_object( $this->_site ) ||
11528 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11529 ) {
11530 return array();
11531 }
11532
11533 return array( $this->_site->plan_id );
11534 }
11535
11536 $plan_ids = array();
11537 $sites = self::get_sites();
11538 foreach ( $sites as $site ) {
11539 $blog_id = self::get_site_blog_id( $site );
11540 $install = $this->get_install_by_blog_id( $blog_id );
11541
11542 if ( ! is_object( $install ) ||
11543 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11544 ) {
11545 continue;
11546 }
11547
11548 $plan_ids[ $install->plan_id ] = true;
11549 }
11550
11551 return array_keys( $plan_ids );
11552 }
11553
11554 /**
11555 * Get a collection of unique license IDs that are associated with any installs in the network.
11556 *
11557 * @author Leo Fajardo (@leorw)
11558 * @since 2.0.0
11559 *
11560 * @return number[]
11561 */
11562 private function get_license_ids_associated_with_installs() {
11563 if ( ! $this->_is_network_active ) {
11564 if ( ! is_object( $this->_site ) ||
11565 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11566 ) {
11567 return array();
11568 }
11569
11570 return array( $this->_site->license_id );
11571 }
11572
11573 $license_ids = array();
11574 $sites = self::get_sites();
11575 foreach ( $sites as $site ) {
11576 $blog_id = self::get_site_blog_id( $site );
11577 $install = $this->get_install_by_blog_id( $blog_id );
11578
11579 if ( ! is_object( $install ) ||
11580 ! FS_Plugin_License::is_valid_id( $install->license_id )
11581 ) {
11582 continue;
11583 }
11584
11585 $license_ids[ $install->license_id ] = true;
11586 }
11587
11588 return array_keys( $license_ids );
11589 }
11590
11591 /**
11592 * @author Vova Feldman (@svovaf)
11593 * @since 1.0.5
11594 *
11595 * @param number $id
11596 *
11597 * @return FS_Plugin_Plan|false
11598 */
11599 function _get_plan_by_id( $id ) {
11600 $this->_logger->entrance();
11601
11602 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11603 $this->_sync_plans();
11604 }
11605
11606 foreach ( $this->_plans as $plan ) {
11607 if ( $id == $plan->id ) {
11608 return $plan;
11609 }
11610 }
11611
11612 return false;
11613 }
11614
11615 /**
11616 * @author Vova Feldman (@svovaf)
11617 * @since 1.1.8.1
11618 *
11619 * @param string $name
11620 *
11621 * @return FS_Plugin_Plan|false
11622 */
11623 private function get_plan_by_name( $name ) {
11624 $this->_logger->entrance();
11625
11626 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11627 $this->_sync_plans();
11628 }
11629
11630 foreach ( $this->_plans as $plan ) {
11631 if ( $name == $plan->name ) {
11632 return $plan;
11633 }
11634 }
11635
11636 return false;
11637 }
11638
11639 /**
11640 * Sync local licenses with remote server.
11641 *
11642 * @author Vova Feldman (@svovaf)
11643 * @since 1.0.6
11644 *
11645 * @param number|bool $site_license_id
11646 * @param number|null $blog_id
11647 *
11648 * @return FS_Plugin_License[]|object
11649 */
11650 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11651 $this->_logger->entrance();
11652
11653 $is_network_admin = fs_is_network_admin();
11654
11655 if ( $is_network_admin && is_null( $blog_id ) ) {
11656 $all_licenses = self::get_all_licenses( $this->_module_id );
11657 } else {
11658 $all_licenses = $this->get_user_licenses( $this->_user->id );
11659 }
11660
11661 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11662
11663 $all_licenses_map = array();
11664 foreach ( $all_licenses as $license ) {
11665 $all_licenses_map[ $license->id ] = true;
11666 }
11667
11668 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11669
11670 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11671 $licenses_map = array();
11672 foreach ( $licenses as $license ) {
11673 $licenses_map[ $license->id ] = true;
11674 }
11675
11676 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11677 // foreach ( $license_ids_to_keep as $license_id ) {
11678 // if ( isset( $licenses_map[ $license_id ] ) ) {
11679 // continue;
11680 // }
11681 //
11682 // $missing_license = self::_get_license_by_id( $license_id, false );
11683 // if ( is_object( $missing_license ) ) {
11684 // $licenses[] = $missing_license;
11685 // $licenses_map[ $missing_license->id ] = true;
11686 // }
11687 // }
11688
11689 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11690
11691 foreach ( $user_license_ids as $key => $license_id ) {
11692 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11693 // Remove access to licenses that no longer exist.
11694 unset( $user_license_ids[ $key ] );
11695 }
11696 }
11697
11698 if ( ! empty( $user_license_ids ) ) {
11699 foreach ( $licenses_map as $license_id => $value ) {
11700 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11701 // Associate new licenses with the user who triggered the license syncing.
11702 $user_license_ids[] = $license_id;
11703 }
11704 }
11705
11706 $user_license_ids = array_unique( $user_license_ids );
11707 } else {
11708 $user_license_ids = array_keys( $licenses_map );
11709 }
11710
11711 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11712 $user_licenses = array();
11713 foreach ( $licenses as $license ) {
11714 if ( ! in_array( $license->id, $user_license_ids ) ) {
11715 continue;
11716 }
11717
11718 $user_licenses[] = $license;
11719 }
11720
11721 $this->_licenses = $user_licenses;
11722 } else {
11723 $this->_licenses = $licenses;
11724 }
11725
11726 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11727
11728 $this->_store_licenses( true, $this->_module_id, $licenses );
11729 }
11730
11731 // Update current license.
11732 if ( is_object( $this->_license ) ) {
11733 $license = $this->_get_license_by_id( $this->_license->id );
11734
11735 if ( is_object( $license ) ) {
11736 /**
11737 * `$license` can be `false` in case a user change action has just been completed and this method
11738 * has synced the `$this->_licenses` collection for the new user. In this case, the
11739 * `$this->_licenses` collection may have only the newly activated license that is associated with
11740 * the new user. `set_license` will eventually be called in the same request by the logic that
11741 * follows outside this method which will detect that the install's license has been updated, and
11742 * then `_update_site_license` will be called which in turn will call `set_license`.
11743 *
11744 * @author Leo Fajardo (@leorw)
11745 * @since 2.3.2
11746 */
11747 $this->set_license( $license );
11748 }
11749 }
11750
11751 return $this->_licenses;
11752 }
11753
11754 /**
11755 * @author Vova Feldman (@svovaf)
11756 * @since 1.0.5
11757 *
11758 * @param number $id
11759 * @param bool $sync_licenses
11760 *
11761 * @return FS_Plugin_License|false
11762 */
11763 function _get_license_by_id( $id, $sync_licenses = true ) {
11764 $this->_logger->entrance();
11765
11766 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11767 return false;
11768 }
11769
11770 /**
11771 * When running from the network level admin and opted-in from the network,
11772 * check if the license exists in the network user licenses collection.
11773 *
11774 * @author Vova Feldman (@svovaf)
11775 * @since 2.0.0
11776 */
11777 if ( fs_is_network_admin() &&
11778 $this->is_network_registered() &&
11779 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11780 ) {
11781 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11782
11783 foreach ( $licenses as $license ) {
11784 if ( $id == $license->id ) {
11785 return $license;
11786 }
11787 }
11788 }
11789
11790 if ( ! $this->has_any_license() && $sync_licenses ) {
11791 $this->_sync_licenses( $id );
11792 }
11793
11794 if ( is_array( $this->_licenses ) ) {
11795 foreach ( $this->_licenses as $license ) {
11796 if ( $id == $license->id ) {
11797 return $license;
11798 }
11799 }
11800 }
11801
11802 return false;
11803 }
11804
11805 /**
11806 * 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.
11807 *
11808 * @author Vova Feldman (@svovaf)
11809 * @since 2.0.0
11810 *
11811 * @param number $id
11812 *
11813 * @return FS_Plugin_License
11814 */
11815 private function get_license_by_id( $id ) {
11816 $licenses = self::get_all_licenses( $this->_module_id );
11817
11818 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11819 foreach ( $licenses as $license ) {
11820 if ( $id == $license->id ) {
11821 return $license;
11822 }
11823 }
11824 }
11825
11826 return null;
11827 }
11828
11829 /**
11830 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11831 *
11832 * @author Vova Feldman (@svovaf)
11833 * @since 2.0.0
11834 *
11835 * @return \FS_Plugin_License|mixed
11836 */
11837 private function sync_site_license() {
11838 $api = $this->get_api_user_scope();
11839
11840 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11841
11842 if ( ! $this->is_api_result_entity( $result ) ) {
11843 return $result;
11844 }
11845
11846 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11847 $this->_store_licenses();
11848
11849 return $license;
11850 }
11851
11852 /**
11853 * Get all user's available licenses for the current module.
11854 *
11855 * @author Vova Feldman (@svovaf)
11856 * @since 2.0.0
11857 *
11858 * @param number $user_id
11859 *
11860 * @return FS_Plugin_License[]
11861 */
11862 private function get_user_licenses( $user_id ) {
11863 $all_licenses = self::get_all_licenses( $this->_module_id );
11864 if ( empty( $all_licenses ) ) {
11865 return array();
11866 }
11867
11868 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
11869 if ( empty( $user_license_ids ) ) {
11870 return array();
11871 }
11872
11873 $licenses = array();
11874 foreach ( $all_licenses as $license ) {
11875 if ( in_array( $license->id, $user_license_ids ) ) {
11876 $licenses[] = $license;
11877 }
11878 }
11879
11880 return $licenses;
11881 }
11882
11883 /**
11884 * Checks if the context license is network activated except on the given blog ID.
11885 *
11886 * @author Vova Feldman (@svovaf)
11887 * @since 2.0.0
11888 *
11889 * @param int $except_blog_id
11890 *
11891 * @return bool
11892 */
11893 private function is_license_network_active( $except_blog_id = 0 ) {
11894 $this->_logger->entrance();
11895
11896 if ( ! is_object( $this->_license ) ) {
11897 return false;
11898 }
11899
11900 $sites = self::get_sites();
11901
11902 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
11903 // There are more sites than the number of activations, so license cannot be network activated.
11904 return false;
11905 }
11906
11907 foreach ( $sites as $site ) {
11908 $blog_id = self::get_site_blog_id( $site );
11909
11910 if ( $except_blog_id == $blog_id ) {
11911 // Skip excluded blog.
11912 continue;
11913 }
11914
11915 $install = $this->get_install_by_blog_id( $blog_id );
11916
11917 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
11918 return false;
11919 }
11920 }
11921
11922 return true;
11923 }
11924
11925 /**
11926 * 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.
11927 *
11928 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
11929 *
11930 * @author Vova Feldman (@svovaf)
11931 * @since 2.0.0
11932 *
11933 * @param \FS_User $user
11934 * @param \FS_Plugin_License $license
11935 *
11936 * @return bool
11937 */
11938 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
11939 $this->_logger->entrance();
11940
11941 $result = $this->can_activate_license_on_network( $license );
11942
11943 if ( false === $result ) {
11944 return false;
11945 }
11946
11947 $installs_without_license = $result['installs'];
11948 if ( ! empty( $installs_without_license ) ) {
11949 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
11950 }
11951
11952 $disconnected_site_ids = $result['sites'];
11953 if ( ! empty( $disconnected_site_ids ) ) {
11954 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
11955 }
11956
11957 $this->link_license_2_user( $license->id, $user->id );
11958
11959 // Sync license after activations.
11960 $license->activated += $result['production_count'];
11961 $license->activated_local += $result['localhost_count'];
11962
11963 // $this->_store_licenses()
11964
11965 return true;
11966 }
11967
11968 /**
11969 * Checks if the given license can be activated on the whole network.
11970 *
11971 * @author Vova Feldman (@svovaf)
11972 * @since 2.0.0
11973 *
11974 * @param \FS_Plugin_License $license
11975 *
11976 * @return false|array {
11977 * @type array[int]FS_Site $installs Blog ID to install map.
11978 * @type int[] $sites Non-connected blog IDs.
11979 * @type int $production_count Production sites count.
11980 * @type int $localhost_count Production sites count.
11981 * }
11982 */
11983 private function can_activate_license_on_network( FS_Plugin_License $license ) {
11984 $sites = self::get_sites();
11985
11986 $production_count = 0;
11987 $localhost_count = 0;
11988
11989 $installs_without_license = array();
11990 $disconnected_site_ids = array();
11991
11992 foreach ( $sites as $site ) {
11993 $blog_id = self::get_site_blog_id( $site );
11994 $install = $this->get_install_by_blog_id( $blog_id );
11995
11996 if ( is_object( $install ) ) {
11997 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
11998 // License already activated on the install.
11999 continue;
12000 }
12001
12002 $url = $install->url;
12003
12004 $installs_without_license[ $blog_id ] = $install;
12005 } else {
12006 $url = is_object( $site ) ?
12007 $site->siteurl :
12008 self::get_unfiltered_site_url( $blog_id );
12009
12010 $disconnected_site_ids[] = $blog_id;
12011 }
12012
12013 if ( FS_Site::is_localhost_by_address( $url ) ) {
12014 $localhost_count ++;
12015 } else {
12016 $production_count ++;
12017 }
12018 }
12019
12020 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12021 return false;
12022 }
12023
12024 return array(
12025 'installs' => $installs_without_license,
12026 'sites' => $disconnected_site_ids,
12027 'production_count' => $production_count,
12028 'localhost_count' => $localhost_count,
12029 );
12030 }
12031
12032 /**
12033 * Activate a given license on a collection of installs.
12034 *
12035 * @author Vova Feldman (@svovaf)
12036 * @since 2.0.0
12037 *
12038 * @param \FS_User $user
12039 * @param string $license_key
12040 * @param array $blog_2_install_map {
12041 * @key int Blog ID.
12042 * @value FS_Site Blog's associated install.
12043 * }
12044 *
12045 * @return mixed|true
12046 */
12047 private function activate_license_on_many_installs(
12048 FS_User $user,
12049 $license_key,
12050 array $blog_2_install_map
12051 ) {
12052 $params = array(
12053 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12054 );
12055
12056 $install_2_blog_map = array();
12057 foreach ( $blog_2_install_map as $blog_id => $install ) {
12058 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12059
12060 $install_2_blog_map[ $install->id ] = $blog_id;
12061 }
12062
12063 $result = $this->get_api_user_scope_by_user( $user )->call(
12064 "plugins/{$this->_plugin->id}/installs.json",
12065 'PUT',
12066 $params
12067 );
12068
12069 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12070 return $result;
12071 }
12072
12073 foreach ( $result->installs as $r_install ) {
12074 $install = new FS_Site( $r_install );
12075 $install->is_disconnected = false;
12076
12077 // Update install.
12078 $this->_store_site(
12079 true,
12080 $install_2_blog_map[ $r_install->id ],
12081 $install
12082 );
12083 }
12084
12085 return true;
12086 }
12087
12088 /**
12089 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12090 *
12091 * @author Vova Feldman (@svovaf)
12092 * @since 2.3.1
12093 *
12094 * @param \FS_User $user
12095 * @param string $license_key
12096 *
12097 * @return true|mixed True if successful, otherwise, the API result.
12098 */
12099 private function activate_license_on_site( FS_User $user, $license_key ) {
12100 return $this->activate_license_on_many_sites( $user, $license_key );
12101 }
12102
12103 /**
12104 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12105 *
12106 * @author Vova Feldman (@svovaf)
12107 * @since 2.0.0
12108 *
12109 * @param \FS_User $user
12110 * @param string $license_key
12111 * @param int[] $site_ids
12112 *
12113 * @return true|mixed True if successful, otherwise, the API result.
12114 */
12115 private function activate_license_on_many_sites(
12116 FS_User $user,
12117 $license_key,
12118 array $site_ids = array()
12119 ) {
12120 $sites = array();
12121 foreach ( $site_ids as $site_id ) {
12122 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12123 }
12124
12125 // Install the plugin.
12126 $result = $this->create_installs_with_user(
12127 $user,
12128 $license_key,
12129 false,
12130 $sites,
12131 false,
12132 true
12133 );
12134
12135 if ( ! $this->is_api_result_entity( $result ) &&
12136 ! $this->is_api_result_object( $result, 'installs' )
12137 ) {
12138 return $result;
12139 }
12140
12141 $installs = array();
12142
12143 if ( $this->is_api_result_entity( $result ) ) {
12144 $install = new FS_Site( $result );
12145
12146 $this->_user = $user;
12147
12148 $this->_store_site( true, null, $install );
12149
12150 $this->_site = $install;
12151
12152 $this->reset_anonymous_mode();
12153 } else {
12154 foreach ( $result->installs as $install ) {
12155 $installs[] = new FS_Site( $install );
12156 }
12157
12158 // Map site addresses to their blog IDs.
12159 $address_to_blog_map = $this->get_address_to_blog_map();
12160
12161 $first_blog_id = null;
12162
12163 foreach ( $installs as $install ) {
12164 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12165 $blog_id = $address_to_blog_map[ $address ];
12166
12167 $this->_store_site( true, $blog_id, $install );
12168
12169 $this->reset_anonymous_mode( $blog_id );
12170
12171 if ( is_null( $first_blog_id ) ) {
12172 $first_blog_id = $blog_id;
12173 }
12174 }
12175
12176 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12177 $this->_storage->network_install_blog_id = $first_blog_id;
12178 }
12179 }
12180
12181 return true;
12182 }
12183
12184 /**
12185 * Sync site's license with user licenses.
12186 *
12187 * @author Vova Feldman (@svovaf)
12188 * @since 1.0.6
12189 *
12190 * @param FS_Plugin_License|null $new_license
12191 *
12192 * @return FS_Plugin_License|null
12193 */
12194 function _update_site_license( $new_license ) {
12195 $this->_logger->entrance();
12196
12197 /**
12198 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12199 * accordingly so that it will also handle the case when an ownership change is done via license
12200 * activation.
12201 *
12202 * @author Leo Fajardo (@leorw)
12203 * @since 2.3.2
12204 */
12205 $this->set_license( $new_license );
12206
12207 if ( ! is_object( $new_license ) ) {
12208 $this->_site->license_id = null;
12209 $this->_sync_site_subscription( null );
12210
12211 return $this->_license;
12212 }
12213
12214 $this->_site->license_id = $this->_license->id;
12215
12216 if ( ! is_array( $this->_licenses ) ) {
12217 $this->_licenses = array();
12218 }
12219
12220 $is_license_found = false;
12221 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12222 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12223 $this->_licenses[ $i ] = $new_license;
12224
12225 $is_license_found = true;
12226 break;
12227 }
12228 }
12229
12230 // If new license just append.
12231 if ( ! $is_license_found ) {
12232 $this->_licenses[] = $new_license;
12233 }
12234
12235 $this->_sync_site_subscription( $new_license );
12236
12237 return $this->_license;
12238 }
12239
12240 /**
12241 * @author Vova Feldman (@svovaf)
12242 * @since 2.3.1
12243 *
12244 * @param \FS_Plugin_License $license
12245 */
12246 private function set_license( FS_Plugin_License $license = null ) {
12247 $this->_license = $license;
12248
12249 $this->maybe_update_whitelabel_flag( $license );
12250 }
12251
12252 /**
12253 * @author Leo Fajardo (@leorw)
12254 * @since 2.3.1
12255 *
12256 * @param FS_Plugin_License $license
12257 */
12258 private function maybe_update_whitelabel_flag( $license ) {
12259 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12260 $this->_storage->is_whitelabeled :
12261 false;
12262
12263 if ( is_object( $license ) ) {
12264 $license_user = self::_get_user_by_id( $license->user_id );
12265
12266 if ( ! is_object( $license_user ) ) {
12267 // If foreign license, do not update the `is_whitelabeled` flag.
12268 return;
12269 }
12270
12271 if ( $this->is_addon() ) {
12272 /**
12273 * Store the last license data to the parent's storage since it's needed only when showing the
12274 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12275 * iterate over the add-ons just to get the last license data.
12276 */
12277 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12278 } else {
12279 $this->store_last_activated_license_data( $license );
12280 }
12281
12282 if ( $license->is_whitelabeled ) {
12283 // Activated a developer license, data should be hidden.
12284 $is_whitelabeled = true;
12285 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12286 // The account owner activated a regular license key, no need to hide the data.
12287 $is_whitelabeled = false;
12288 }
12289 }
12290
12291 $this->_storage->is_whitelabeled = $is_whitelabeled;
12292
12293 // Reset the whitelabeled status after update.
12294 $this->is_whitelabeled = null;
12295 if ( $this->is_addon() ) {
12296 $parent_fs = $this->get_parent_instance();
12297
12298 if ( is_object( $parent_fs ) ) {
12299 $parent_fs->is_whitelabeled = null;
12300 }
12301 }
12302 }
12303
12304 /**
12305 * @author Leo Fajardo (@leorw)
12306 * @since 2.3.1
12307 *
12308 * @param FS_Plugin_License $license
12309 * @param FS_User $license_user
12310 */
12311 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12312 if ( ! is_object( $license_user ) ) {
12313 $this->_storage->last_license_key = md5( $license->secret_key );
12314 $this->_storage->last_license_user_id = null;
12315 } else {
12316 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12317 $this->_storage->last_license_user_id = $license_user->id;
12318 }
12319 }
12320
12321 /**
12322 * @author Leo Fajardo (@leorw)
12323 * @since 2.3.1
12324 *
12325 * @param bool $ignore_data_debug_mode
12326 *
12327 * @return bool
12328 */
12329 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12330 if ( true !== $this->_storage->is_whitelabeled ) {
12331 return false;
12332 } else if ( $ignore_data_debug_mode ) {
12333 return true;
12334 }
12335
12336 $fs = $this->is_addon() ?
12337 $this->get_parent_instance() :
12338 $this;
12339
12340 return ! $fs->is_data_debug_mode();
12341 }
12342
12343 /**
12344 * @author Leo Fajardo (@leorw)
12345 * @since 2.3.1
12346 *
12347 * @return number
12348 */
12349 function get_last_license_user_id() {
12350 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12351 $this->_storage->last_license_user_id :
12352 null;
12353 }
12354
12355 /**
12356 * @author Leo Fajardo (@leorw)
12357 * @since 2.3.1
12358 *
12359 * @param int $blog_id
12360 * @param bool $ignore_data_debug_mode
12361 *
12362 * @return bool
12363 */
12364 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12365 if ( ! is_null( $blog_id ) ) {
12366 $this->switch_to_blog( $blog_id );
12367 }
12368
12369 if ( ! is_null( $this->is_whitelabeled ) ) {
12370 $is_whitelabeled = $this->is_whitelabeled;
12371 } else {
12372 $is_whitelabeled = false;
12373
12374 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12375
12376 if ( ! $this->has_addons() ) {
12377 $is_whitelabeled = $is_whitelabeled_flag;
12378 } else if ( $is_whitelabeled_flag ) {
12379 $is_whitelabeled = true;
12380 } else {
12381 if ( $this->is_registered() || $this->is_premium() ) {
12382 $addon_ids = $this->get_updated_account_addons();
12383 } else {
12384 $addons = self::get_all_addons();
12385
12386 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12387 $addons[ $this->_plugin->id ] :
12388 array();
12389
12390 $addon_ids = array();
12391 foreach ( $plugin_addons as $addon ) {
12392 $addon_ids[] = $addon->id;
12393 }
12394 }
12395
12396 $installed_addons = $this->get_installed_addons();
12397 foreach ( $installed_addons as $fs_addon ) {
12398 $addon_ids[] = $fs_addon->get_id();
12399 }
12400
12401 if ( ! empty( $addon_ids ) ) {
12402 $addon_ids = array_unique( $addon_ids );
12403
12404 $is_network_level = (
12405 fs_is_network_admin() &&
12406 $this->is_network_active()
12407 );
12408
12409 foreach ( $addon_ids as $addon_id ) {
12410 $addon = $this->get_addon( $addon_id );
12411
12412 if ( ! is_object( $addon ) ) {
12413 continue;
12414 }
12415
12416 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12417 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12418 self::get_addon_instance( $addon_id ) :
12419 null;
12420
12421 $was_addon_network_activated = false;
12422
12423 if ( is_object( $fs_addon ) ) {
12424 $was_addon_network_activated = $fs_addon->is_network_active();
12425 } else if ( $is_network_level ) {
12426 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12427 }
12428
12429 $network_delegated_connection = (
12430 $was_addon_network_activated &&
12431 $addon_storage->get( 'is_delegated_connection', false, true )
12432 );
12433
12434 if (
12435 $is_network_level &&
12436 ( ! $was_addon_network_activated || $network_delegated_connection )
12437 ) {
12438 $sites = self::get_sites();
12439
12440 /**
12441 * If in network admin area and the add-on was not network-activated or network-activated
12442 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12443 */
12444 foreach ( $sites as $site ) {
12445 $site_info = $this->get_site_info( $site );
12446
12447 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12448 $is_whitelabeled = true;
12449 break;
12450 }
12451 }
12452
12453 if ( $is_whitelabeled ) {
12454 break;
12455 }
12456 } else {
12457 /**
12458 * This will be executed when any of the following is met:
12459 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12460 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12461 * 3. Add-on was not network-activated and in site admin area.
12462 */
12463 if ( true === $addon_storage->is_whitelabeled ) {
12464 $is_whitelabeled = true;
12465 break;
12466 }
12467 }
12468 }
12469 }
12470 }
12471
12472 $this->is_whitelabeled = $is_whitelabeled;
12473
12474 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12475 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12476 }
12477
12478 if ( ! is_null( $blog_id ) ) {
12479 $this->restore_current_blog();
12480 }
12481 }
12482
12483 return (
12484 $is_whitelabeled &&
12485 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12486 );
12487 }
12488
12489 /**
12490 * Sync site's subscription.
12491 *
12492 * @author Vova Feldman (@svovaf)
12493 * @since 1.0.9
12494 *
12495 * @param FS_Plugin_License|null $license
12496 *
12497 * @return bool|\FS_Subscription
12498 */
12499 private function _sync_site_subscription( $license ) {
12500 if ( ! is_object( $license ) ) {
12501 $this->delete_unused_subscriptions();
12502
12503 return false;
12504 }
12505
12506 // Load subscription details if not lifetime.
12507 $subscription = $license->is_lifetime() ?
12508 false :
12509 $this->_fetch_site_license_subscription();
12510
12511 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12512 $this->store_subscription( $subscription );
12513 } else {
12514 $this->delete_unused_subscriptions();
12515 }
12516
12517 return $subscription;
12518 }
12519
12520 /**
12521 * @author Vova Feldman (@svovaf)
12522 * @since 1.0.6
12523 *
12524 * @return bool|\FS_Plugin_License
12525 */
12526 function _get_license() {
12527 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12528 return $this->_license;
12529 }
12530
12531 return $this->_get_available_premium_license();
12532 }
12533
12534 /**
12535 * @param number $license_id
12536 *
12537 * @return null|\FS_Subscription
12538 */
12539 function _get_subscription( $license_id ) {
12540 if ( ! isset( $this->_storage->subscriptions ) ||
12541 empty( $this->_storage->subscriptions )
12542 ) {
12543 return null;
12544 }
12545
12546 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12547 if ( $subscription->license_id == $license_id ) {
12548 return $subscription;
12549 }
12550 }
12551
12552 return null;
12553 }
12554
12555 /**
12556 * @author Leo Fajardo (@leorw)
12557 * @since 2.0.0
12558 *
12559 * @param FS_Subscription $subscription
12560 */
12561 function store_subscription( FS_Subscription $subscription ) {
12562 if ( ! isset( $this->_storage->subscriptions ) ) {
12563 $this->_storage->subscriptions = array();
12564 }
12565
12566 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12567 $this->_storage->subscriptions = array( $subscription );
12568
12569 return;
12570 }
12571
12572 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12573
12574 $updated_subscription = false;
12575 foreach ( $subscriptions as $key => $existing_subscription ) {
12576 if ( $existing_subscription->id == $subscription->id ) {
12577 $subscriptions[ $key ] = $subscription;
12578 $updated_subscription = true;
12579 break;
12580 }
12581 }
12582
12583 if ( ! $updated_subscription ) {
12584 $subscriptions[] = $subscription;
12585 }
12586
12587 $this->_storage->subscriptions = $subscriptions;
12588 }
12589
12590 /**
12591 * @author Leo Fajardo (@leorw)
12592 * @since 2.0.0
12593 */
12594 function delete_unused_subscriptions() {
12595 if ( ! isset( $this->_storage->subscriptions ) ||
12596 empty( $this->_storage->subscriptions ) ||
12597 // Clean up only if there are already at least 3 subscriptions.
12598 ( count( $this->_storage->subscriptions ) < 3 )
12599 ) {
12600 return;
12601 }
12602
12603 if ( ! is_multisite() ) {
12604 // If not multisite, there should only be 1 subscription, so just clear the array.
12605 $this->_storage->subscriptions = array();
12606
12607 return;
12608 }
12609
12610 $subscriptions_to_keep_by_license_id_map = array();
12611 $sites = self::get_sites();
12612 foreach ( $sites as $site ) {
12613 $blog_id = self::get_site_blog_id( $site );
12614 $install = $this->get_install_by_blog_id( $blog_id );
12615
12616 if ( ! is_object( $install ) ||
12617 ! FS_Plugin_License::is_valid_id( $install->license_id )
12618 ) {
12619 continue;
12620 }
12621
12622 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12623 }
12624
12625 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12626 $this->_storage->subscriptions = array();
12627
12628 return;
12629 }
12630
12631 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12632 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12633 unset( $this->_storage->subscriptions[ $key ] );
12634 }
12635 }
12636 }
12637
12638 /**
12639 * @author Vova Feldman (@svovaf)
12640 * @since 1.0.2
12641 *
12642 * @param string $plan Plan name
12643 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12644 *
12645 * @return bool
12646 */
12647 function is_plan( $plan, $exact = false ) {
12648 $this->_logger->entrance();
12649
12650 if ( ! $this->is_registered() ) {
12651 return false;
12652 }
12653
12654 $plan = strtolower( $plan );
12655
12656 $current_plan_name = $this->get_plan_name();
12657
12658 if ( $current_plan_name === $plan ) {
12659 // Exact plan.
12660 return true;
12661 } else if ( $exact ) {
12662 // Required exact, but plans are different.
12663 return false;
12664 }
12665
12666 $current_plan_order = - 1;
12667 $required_plan_order = PHP_INT_MAX;
12668 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12669 if ( $plan === $this->_plans[ $i ]->name ) {
12670 $required_plan_order = $i;
12671 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12672 $current_plan_order = $i;
12673 }
12674 }
12675
12676 return ( $current_plan_order > $required_plan_order );
12677 }
12678
12679 /**
12680 * Check if module has only one plan.
12681 *
12682 * @author Vova Feldman (@svovaf)
12683 * @since 1.2.1.7
12684 *
12685 * @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.
12686 *
12687 * @return bool
12688 */
12689 function is_single_plan( $double_check = false ) {
12690 $this->_logger->entrance();
12691
12692 if ( ! $this->is_registered() ||
12693 ! is_array( $this->_plans ) ||
12694 0 === count( $this->_plans )
12695 ) {
12696 return true;
12697 }
12698
12699 $has_free_plan = $this->has_free_plan();
12700
12701 if ( ! $has_free_plan && $double_check ) {
12702 foreach ( $this->_plans as $plan ) {
12703 if ( $plan->is_free() ) {
12704 $has_free_plan = true;
12705 break;
12706 }
12707 }
12708 }
12709
12710 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12711 }
12712
12713 /**
12714 * Check if plan based on trial. If not in trial mode, should return false.
12715 *
12716 * @since 1.0.9
12717 *
12718 * @param string $plan Plan name
12719 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12720 *
12721 * @return bool
12722 */
12723 function is_trial_plan( $plan, $exact = false ) {
12724 $this->_logger->entrance();
12725
12726 if ( ! $this->is_registered() ) {
12727 return false;
12728 }
12729
12730 if ( ! $this->is_trial() ) {
12731 return false;
12732 }
12733
12734 $trial_plan = $this->get_trial_plan();
12735
12736 if ( $trial_plan->name === $plan ) {
12737 // Exact plan.
12738 return true;
12739 } else if ( $exact ) {
12740 // Required exact, but plans are different.
12741 return false;
12742 }
12743
12744 $current_plan_order = - 1;
12745 $required_plan_order = - 1;
12746 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12747 if ( $plan === $this->_plans[ $i ]->name ) {
12748 $required_plan_order = $i;
12749 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12750 $current_plan_order = $i;
12751 }
12752 }
12753
12754 return ( $current_plan_order > $required_plan_order );
12755 }
12756
12757 /**
12758 * Check if plugin has any paid plans.
12759 *
12760 * @author Vova Feldman (@svovaf)
12761 * @since 1.0.7
12762 *
12763 * @return bool
12764 */
12765 function has_paid_plan() {
12766 return $this->_has_paid_plans ||
12767 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12768 }
12769
12770 /**
12771 * Check if plugin has any plan with a trail.
12772 *
12773 * @author Vova Feldman (@svovaf)
12774 * @since 1.0.9
12775 *
12776 * @return bool
12777 */
12778 function has_trial_plan() {
12779 /**
12780 * @author Vova Feldman(@svovaf)
12781 * @since 1.2.1.5
12782 *
12783 * Allow setting a trial from the SDK without calling the API.
12784 * But, if the user did opt-in, continue using the real data from the API.
12785 */
12786 if ( $this->_trial_days >= 0 ) {
12787 return true;
12788 }
12789
12790 return $this->_storage->get( 'has_trial_plan', false );
12791 }
12792
12793 /**
12794 * Check if plugin has any free plan, or is it premium only.
12795 *
12796 * Note: If no plans configured, assume plugin is free.
12797 *
12798 * @author Vova Feldman (@svovaf)
12799 * @since 1.0.7
12800 *
12801 * @return bool
12802 */
12803 function has_free_plan() {
12804 return ! $this->is_only_premium();
12805 }
12806
12807 /**
12808 * Displays a license activation dialog box when the user clicks on the "Activate License"
12809 * or "Change License" link on the plugins
12810 * page.
12811 *
12812 * @author Leo Fajardo (@leorw)
12813 * @since 1.1.9
12814 */
12815 function _add_license_activation_dialog_box() {
12816 $vars = array(
12817 'id' => $this->_module_id,
12818 );
12819
12820 fs_require_template( 'forms/license-activation.php', $vars );
12821 fs_require_template( 'forms/resend-key.php', $vars );
12822 }
12823
12824 /**
12825 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
12826 *
12827 * @author Leo Fajardo (@leorw)
12828 * @since 2.5.0
12829 */
12830 function _add_email_address_update_dialog_box() {
12831 $vars = array( 'id' => $this->_module_id );
12832
12833 fs_require_template( 'forms/email-address-update.php', $vars );
12834 }
12835
12836 /**
12837 * @author Leo Fajardo (@leorw)
12838 * @since 2.5.0
12839 */
12840 function _add_email_address_update_option() {
12841 if ( ! $this->should_handle_user_change() ) {
12842 return;
12843 }
12844
12845 // Add email address update AJAX handler.
12846 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
12847 }
12848
12849 /**
12850 * @author Leo Fajardo (@leorw)
12851 * @since 2.5.0
12852 */
12853 function _email_address_update_ajax_handler() {
12854 $this->check_ajax_referer( 'update_email_address' );
12855
12856 $new_email_address = fs_request_get( 'email_address' );
12857 $transfer_type = fs_request_get( 'transfer_type' );
12858
12859 $result = $this->update_email( $new_email_address );
12860
12861 if ( ! FS_Api::is_api_error( $result ) ) {
12862 self::shoot_ajax_success();
12863 }
12864
12865 $error = '';
12866
12867 if ( FS_Api::is_api_error_object( $result ) ) {
12868 switch ( $result->error->code ) {
12869 case 'user_exist':
12870 case 'account_verification_required':
12871 $error = array(
12872 'code' => 'change_ownership',
12873 'url' => $this->get_account_url( 'change_owner', array(
12874 'state' => 'init',
12875 'candidate_email' => $new_email_address,
12876 'transfer_type' => $transfer_type,
12877 ) ),
12878 );
12879
12880 break;
12881 }
12882 }
12883
12884 if ( empty( $error ) ) {
12885 $error = is_object( $result ) ?
12886 var_export( $result->error, true ) :
12887 $result;
12888 }
12889
12890 self::shoot_ajax_failure( $error );
12891 }
12892
12893 /**
12894 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
12895 *
12896 * @author Leo Fajardo (@leorw)
12897 * @since 2.3.2
12898 *
12899 * @return number[]
12900 */
12901 function get_installs_ids_with_foreign_licenses() {
12902 $installs = array();
12903
12904 if (
12905 is_object( $this->_license ) &&
12906 $this->_site->user_id != $this->_license->user_id
12907 ) {
12908 $installs[] = $this->_site->id;
12909 }
12910
12911 /**
12912 * Also try to get foreign licenses for the context product's add-ons.
12913 */
12914 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
12915
12916 foreach ( $installs_by_slug_map as $slug => $install_info ) {
12917 if ( $slug == $this->get_slug() ) {
12918 continue;
12919 }
12920
12921 $install = $install_info['install'];
12922 $license = $install_info['license'];
12923
12924 if (
12925 is_object( $license ) &&
12926 $install->user_id != $license->user_id
12927 ) {
12928 $installs[] = $install->id;
12929 }
12930 }
12931
12932 return $installs;
12933 }
12934
12935 /**
12936 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
12937 *
12938 * @author Leo Fajardo (@leorw)
12939 * @since 2.3.2
12940 *
12941 * @param number[] $install_ids
12942 */
12943 function _add_user_change_dialog_box( $install_ids ) {
12944 $vars = array(
12945 'id' => $this->_module_id,
12946 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
12947 );
12948
12949 fs_require_template( 'forms/user-change.php', $vars );
12950 }
12951
12952 /**
12953 * @author Leo Fajardo (@leorw)
12954 * @since 2.3.1
12955 */
12956 function _add_data_debug_mode_dialog_box() {
12957 $vars = array(
12958 'id' => $this->_module_id,
12959 );
12960
12961 fs_require_template( 'forms/data-debug-mode.php', $vars );
12962 }
12963
12964 /**
12965 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
12966 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
12967 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
12968 * is only activated on a single production site.
12969 *
12970 * @author Leo Fajardo (@leorw)
12971 * @since 2.2.1
12972 *
12973 * @param bool $is_license_deactivation
12974 *
12975 * @return array
12976 */
12977 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
12978 if ( fs_is_network_admin() ) {
12979 // Subscription cancellation dialog box is currently not supported for multisite networks.
12980 return array();
12981 }
12982
12983 if ( $this->is_whitelabeled() ) {
12984 return array();
12985 }
12986
12987 $license = $this->_get_license();
12988
12989 /**
12990 * 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.
12991 *
12992 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
12993 * @since 2.2.1
12994 */
12995 if ( ! is_object( $license ) ||
12996 $license->is_lifetime() ||
12997 ( ! $license->is_single_site() && $license->activated > 1 )
12998 ) {
12999 return array();
13000 }
13001
13002 /**
13003 * @var FS_Subscription $subscription
13004 */
13005 $subscription = $this->_get_subscription( $license->id );
13006 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13007 return array();
13008 }
13009
13010 return array(
13011 'id' => $this->_module_id,
13012 'license' => $license,
13013 'has_trial' => $this->is_paid_trial(),
13014 'is_license_deactivation' => $is_license_deactivation,
13015 );
13016 }
13017
13018 /**
13019 * @author Leo Fajardo (@leorw)
13020 * @since 2.0.2
13021 */
13022 function _add_premium_version_upgrade_selection_dialog_box() {
13023 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13024 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13025 return;
13026 }
13027
13028 $vars = array(
13029 'id' => $this->_module_id,
13030 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13031 $modules_update->response[ $this->_plugin_basename ]->new_version :
13032 $modules_update->response[ $this->_plugin_basename ]['new_version']
13033 );
13034
13035 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13036 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13037 }
13038
13039 /**
13040 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13041 * page.
13042 *
13043 * @author Leo Fajardo (@leorw)
13044 * @since 1.2.1.5
13045 */
13046 function _add_optout_dialog() {
13047 if ( $this->is_theme() ) {
13048 $vars = null;
13049 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13050 }
13051
13052 $vars = array( 'id' => $this->_module_id );
13053 fs_require_template( 'forms/optout.php', $vars );
13054 }
13055
13056 /**
13057 * Prepare page to include all required UI and logic for the license activation dialog.
13058 *
13059 * @author Vova Feldman (@svovaf)
13060 * @since 1.2.0
13061 */
13062 function _add_license_activation() {
13063 if ( $this->is_migration() ) {
13064 return;
13065 }
13066
13067 if ( ! $this->is_user_admin() ) {
13068 // Only admins can activate a license.
13069 return;
13070 }
13071
13072 if ( ! $this->has_paid_plan() ) {
13073 // Module doesn't have any paid plans.
13074 return;
13075 }
13076
13077 if (
13078 $this->has_premium_version() &&
13079 ! $this->is_premium() &&
13080 /**
13081 * Also handle the case when an upgrade was made using the free version.
13082 *
13083 * @author Leo Fajardo (@leorw)
13084 * @since 2.3.2
13085 */
13086 ! is_object( $this->_get_license() )
13087 ) {
13088 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13089 return;
13090 }
13091
13092 // Add license activation link and AJAX request handler.
13093 if ( self::is_plugins_page() ) {
13094 $is_network_admin = fs_is_network_admin();
13095
13096 if (
13097 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13098 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13099 ) {
13100 if (
13101 $this->is_premium() ||
13102 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13103 ) {
13104 /**
13105 * @since 1.2.0 Add license action link only on plugins page.
13106 */
13107 $this->_add_license_action_link();
13108 }
13109 }
13110 }
13111
13112 // Add license activation AJAX callback.
13113 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13114
13115 // Add resend license AJAX callback.
13116 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13117 }
13118
13119 /**
13120 * Prepares page to include all required UI and logic for the "Change User" dialog.
13121 *
13122 * @author Leo Fajardo (@leorw)
13123 * @since 2.3.2
13124 */
13125 function _add_user_change_option() {
13126 if ( ! $this->should_handle_user_change() ) {
13127 return;
13128 }
13129
13130 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13131
13132 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13133 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13134 return;
13135 }
13136
13137 // Add user change AJAX handler.
13138 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13139 }
13140
13141 /**
13142 * @author Leo Fajardo (@leorw)
13143 * @since 2.3.2
13144 */
13145 function should_handle_user_change() {
13146 if ( ! $this->is_user_admin() ) {
13147 // Only admins can change user.
13148 return false;
13149 }
13150
13151 if ( $this->is_addon() ) {
13152 return false;
13153 }
13154
13155 if ( ! $this->is_registered() ) {
13156 return false;
13157 }
13158
13159 if (
13160 $this->is_network_active() &&
13161 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13162 ) {
13163 // Handle only on site-level "Account" section for now.
13164 return false;
13165 }
13166
13167 return true;
13168 }
13169
13170 /**
13171 * @author Leo Fajardo (@leorw)
13172 * @since 2.0.2
13173 */
13174 function _add_premium_version_upgrade_selection() {
13175 if ( ! $this->is_user_admin() ) {
13176 return;
13177 }
13178
13179 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13180 // This is relevant only to the free versions and premium versions without an active license.
13181 return;
13182 }
13183
13184 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13185 $this->_add_premium_version_upgrade_selection_action();
13186 }
13187 }
13188
13189 /**
13190 * @author Edgar Melkonyan
13191 * @since 2.4.1
13192 *
13193 * @throws Freemius_Exception
13194 */
13195 function _toggle_whitelabel_mode_ajax_handler() {
13196 $this->_logger->entrance();
13197
13198 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13199
13200 if ( ! $this->is_user_admin() ) {
13201 // Only for admins.
13202 self::shoot_ajax_failure();
13203 }
13204
13205 $license = $this->get_api_user_scope()->call(
13206 "/licenses/{$this->_site->license_id}.json",
13207 'put',
13208 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13209 );
13210
13211 if ( ! $this->is_api_result_entity( $license ) ) {
13212 self::shoot_ajax_failure(
13213 FS_Api::is_api_error_object( $license ) ?
13214 $license->error->message :
13215 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13216 );
13217 }
13218
13219 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13220 $this->_store_licenses();
13221
13222 $this->_sync_license();
13223
13224 if ( ! $license->is_whitelabeled ) {
13225 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13226 } else {
13227 $this->_admin_notices->add_sticky(
13228 sprintf(
13229 $this->get_text_inline(
13230 '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.',
13231 'license_whitelabeled'
13232 ),
13233 "<strong>{$this->get_plugin_title()}</strong>",
13234 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13235 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13236 ),
13237 'license_whitelabeled'
13238 );
13239 }
13240
13241 self::shoot_ajax_response( array( 'success' => true ) );
13242 }
13243
13244 /**
13245 * @author Leo Fajardo (@leorw)
13246 * @since 2.3.0
13247 */
13248 function _add_beta_mode_update_handler() {
13249 if ( ! $this->is_user_admin() ) {
13250 return;
13251 }
13252
13253 if ( ! $this->is_premium() ) {
13254 return;
13255 }
13256
13257 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13258 }
13259
13260 /**
13261 * @author Leo Fajardo (@leorw)
13262 * @since 2.3.0
13263 */
13264 function _set_beta_mode_ajax_handler() {
13265 $this->_logger->entrance();
13266
13267 $this->check_ajax_referer( 'set_beta_mode' );
13268
13269 if ( ! $this->is_user_admin() ) {
13270 // Only for admins.
13271 self::shoot_ajax_failure();
13272 }
13273
13274 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13275
13276 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13277 self::shoot_ajax_failure();
13278 }
13279
13280 $site = $this->api_site_call(
13281 '',
13282 'put',
13283 array(
13284 'is_beta' => ( 'true' == $is_beta ),
13285 'fields' => 'is_beta'
13286 )
13287 );
13288
13289 if ( ! $this->is_api_result_entity( $site ) ) {
13290 self::shoot_ajax_failure(
13291 FS_Api::is_api_error_object( $site ) ?
13292 $site->error->message :
13293 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13294 );
13295 }
13296
13297 $this->_site->is_beta = $site->is_beta;
13298 $this->_store_site();
13299
13300 self::shoot_ajax_response( array( 'success' => true ) );
13301 }
13302
13303 /**
13304 * License activation WP AJAX handler.
13305 *
13306 * @author Leo Fajardo (@leorw)
13307 * @since 1.1.9
13308 *
13309 * @uses Freemius::activate_license()
13310 */
13311 function _activate_license_ajax_action() {
13312 $this->_logger->entrance();
13313
13314 $this->check_ajax_referer( 'activate_license' );
13315
13316 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13317
13318 if ( empty( $license_key ) ) {
13319 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13320
13321 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13322 $license = $this->_get_license_by_id( $license_id, false );
13323
13324 if ( is_object( $license ) ) {
13325 $license_key = $license->secret_key;
13326 }
13327 }
13328 }
13329
13330 if ( empty( $license_key ) ) {
13331 exit;
13332 }
13333
13334 $sites = fs_is_network_admin() ?
13335 fs_request_get( 'sites', array(), 'post' ) :
13336 array();
13337
13338 $result = $this->activate_license(
13339 $license_key,
13340 $sites,
13341 fs_request_get_bool( 'is_marketing_allowed', null ),
13342 fs_request_get( 'blog_id', null ),
13343 fs_request_get( 'module_id', null, 'post' ),
13344 fs_request_get( 'user_id', null ),
13345 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13346 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13347 );
13348
13349 if (
13350 $result['success'] &&
13351 $this->is_bundle_license_auto_activation_enabled()
13352 ) {
13353 $license = new FS_Plugin_License();
13354 $license->secret_key = $license_key;
13355
13356 $this->maybe_activate_bundle_license( $license, $sites );
13357 }
13358
13359 echo json_encode( $result );
13360
13361 exit;
13362 }
13363
13364 /**
13365 * User change WP AJAX handler.
13366 *
13367 * @author Leo Fajardo (@leorw)
13368 * @since 2.3.2
13369 */
13370 function _user_change_ajax_action() {
13371 $this->_logger->entrance();
13372
13373 $this->check_ajax_referer( 'change_user' );
13374
13375 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13376 $new_user_id = fs_request_get( 'user_id' );
13377
13378 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13379 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13380 }
13381
13382 $params = array();
13383
13384 if ( ! empty( $new_email_address ) ) {
13385 $params['user_email'] = $new_email_address;
13386 } else {
13387 $params['user_id'] = $new_user_id;
13388 }
13389
13390 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13391 $install_ids = array();
13392
13393 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13394 $install_ids[ $slug ] = $install_info['install']->id;
13395 }
13396
13397 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13398
13399 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13400
13401 if ( FS_Api::is_api_error( $install ) ) {
13402 $error = '';
13403
13404 if ( is_object( $install ) ) {
13405 switch ( $install->error->code ) {
13406 case 'user_exist':
13407 $error = (
13408 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13409 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13410 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>' ) .
13411 sprintf(
13412 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13413 $this->get_account_url( 'change_owner', array(
13414 'state' => 'init',
13415 'candidate_email' => $new_email_address
13416 ) ),
13417 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13418 )
13419 );
13420 break;
13421 }
13422 }
13423
13424 if ( empty( $error ) ) {
13425 $error = FS_Api::is_api_error_object( $install ) ?
13426 $install->error->message :
13427 var_export( $install->error, true );
13428 }
13429
13430 self::shoot_ajax_failure( $error );
13431 } else {
13432 if (
13433 // If successful ownership change.
13434 $this->get_user()->id != $install->user_id ||
13435 ! empty( $new_email_address )
13436 ) {
13437 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13438 }
13439 }
13440
13441 self::shoot_ajax_success();
13442 }
13443
13444 /**
13445 * @author Leo Fajardo (@leorw)
13446 * @since 2.3.2.14
13447 */
13448 function starting_migration() {
13449 if ( ! empty( $this->_storage->license_migration ) ) {
13450 // Do not overwrite the data if already set.
13451 return;
13452 }
13453
13454 $this->_storage->license_migration = array(
13455 'is_migrating' => true,
13456 'start_timestamp' => time()
13457 );
13458 }
13459
13460 /**
13461 * @author Leo Fajardo (@leorw)
13462 * @since 2.3.2.14
13463 */
13464 function is_migration() {
13465 if ( $this->is_addon() ) {
13466 return $this->get_parent_instance()->is_migration();
13467 }
13468
13469 if ( empty( $this->_storage->license_migration ) ) {
13470 return false;
13471 }
13472
13473 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13474 return false;
13475 }
13476
13477 return (
13478 // Return `true` if the migration is within 5 minutes from the starting time.
13479 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13480 );
13481 }
13482
13483 /**
13484 *
13485 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13486 *
13487 * @author Vova Feldman (@svovaf)
13488 * @since 2.3.0
13489 *
13490 * @param string $license_key
13491 * @param null|bool $is_marketing_allowed
13492 * @param null|number $plugin_id
13493 * @param array $sites
13494 * @param int $blog_id
13495 *
13496 * @return array {
13497 * @var bool $success
13498 * @var string $error
13499 * @var string $next_page
13500 * }
13501 *
13502 * @uses Freemius::activate_license()
13503 */
13504 function activate_migrated_license(
13505 $license_key,
13506 $is_marketing_allowed = null,
13507 $plugin_id = null,
13508 $sites = array(),
13509 $blog_id = null
13510 ) {
13511 $this->_logger->entrance();
13512
13513 $result = $this->activate_license(
13514 $license_key,
13515 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13516 $this->get_sites_for_network_level_optin() :
13517 $sites,
13518 $is_marketing_allowed,
13519 $blog_id,
13520 $plugin_id
13521 );
13522
13523 // No need to show the sticky after license activation notice after migrating a license.
13524 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13525
13526 return $result;
13527 }
13528
13529 /**
13530 * @author Leo Fajardo (@leorw)
13531 * @since 2.3.1
13532 *
13533 * @return string
13534 */
13535 function get_pricing_js_path() {
13536 if ( ! isset( $this->_pricing_js_path ) ) {
13537 $default_path = WP_FS__DIR_JS . '/pricing/freemius-pricing.js';
13538
13539 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', $default_path );
13540
13541 // Backward compatibility for people who placed the freemius-pricing inside `includes` directory. Let it take more preference than the default path.
13542 if ( empty( $pricing_js_path ) ) {
13543 global $fs_active_plugins;
13544
13545 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13546 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13547 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13548
13549 $pricing_js_path = $plugin_or_theme_root_dir
13550 . '/'
13551 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13552 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13553 . '/includes/freemius-pricing/freemius-pricing.js';
13554
13555 break;
13556 }
13557 }
13558 }
13559
13560 // If it is still empty, load the default pricing JS.
13561 if ( ! file_exists( $pricing_js_path ) ) {
13562 $pricing_js_path = $default_path;
13563 }
13564
13565 $this->_pricing_js_path = $pricing_js_path;
13566 }
13567
13568 return $this->_pricing_js_path;
13569 }
13570
13571 /**
13572 * @author Leo Fajardo (@leorw)
13573 * @since 2.3.1
13574 *
13575 * @deprecated Since v2.9.0 we have removed the iFrame based pricing. This will always return `false`.
13576 *
13577 * @return bool
13578 */
13579 function should_use_external_pricing() {
13580 return false;
13581 }
13582
13583 /**
13584 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13585 *
13586 * @author Vova Feldman (@svovaf)
13587 * @since 2.2.4
13588 * @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).
13589 * @param string $license_key
13590 * @param array $sites
13591 * @param null|bool $is_marketing_allowed
13592 * @param null|int $blog_id
13593 * @param null|number $plugin_id
13594 * @param null|number $license_owner_id
13595 * @param bool|null $is_extensions_tracking_allowed
13596 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13597 *
13598 *
13599 * @return array {
13600 * @var bool $success
13601 * @var string $error
13602 * @var string $next_page
13603 * }
13604 */
13605 private function activate_license(
13606 $license_key,
13607 $sites = array(),
13608 $is_marketing_allowed = null,
13609 $blog_id = null,
13610 $plugin_id = null,
13611 $license_owner_id = null,
13612 $is_extensions_tracking_allowed = null,
13613 $is_diagnostic_tracking_allowed = null
13614 ) {
13615 $this->_logger->entrance();
13616
13617 $license_key = trim( $license_key );
13618
13619 $is_network_activation_or_migration = (
13620 fs_is_network_admin() ||
13621 ( ! empty( $sites ) && $this->is_migration() )
13622 );
13623
13624 if ( ! $is_network_activation_or_migration ) {
13625 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13626 $sites = array();
13627 }
13628
13629 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13630 $this :
13631 $this->get_addon_instance( $plugin_id );
13632
13633 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
13634 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
13635 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
13636 ) );
13637
13638 $error = false;
13639 $next_page = false;
13640
13641 $has_valid_blog_id = is_numeric( $blog_id );
13642
13643 $user = null;
13644
13645 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13646 /**
13647 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13648 *
13649 * @author Vova Feldman (@svovaf)
13650 */
13651 $user = $fs->get_parent_instance()->get_current_or_network_user();
13652 } else if ( $fs->is_registered() ) {
13653 $user = $fs->get_current_or_network_user();
13654 }
13655
13656 if ( $has_valid_blog_id ) {
13657 /**
13658 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13659 *
13660 * @author Leo Fajardo (@leorw)
13661 */
13662 $fs->switch_to_blog( $blog_id );
13663 }
13664
13665 if ( is_object( $user ) ) {
13666 $result = true;
13667
13668 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13669 // If no specific blog ID was provided, activate the license for all sites in the network.
13670 $blog_2_install_map = array();
13671 $site_ids = array();
13672
13673 foreach ( $sites as $site ) {
13674 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13675 continue;
13676 }
13677
13678 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13679
13680 if ( is_object( $install ) ) {
13681 $blog_2_install_map[ $site['blog_id'] ] = $install;
13682 } else {
13683 $site_ids[] = $site['blog_id'];
13684 }
13685 }
13686
13687 if ( ! empty( $blog_2_install_map ) ) {
13688 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13689 }
13690
13691 if ( true === $result && ! empty( $site_ids ) ) {
13692 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13693 }
13694 } else {
13695 if ( $fs->is_registered() ) {
13696 $params = array(
13697 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13698 );
13699
13700 $install_ids = array();
13701
13702 $change_owner = FS_User::is_valid_id( $license_owner_id );
13703
13704 if ( $change_owner ) {
13705 $params['user_id'] = $license_owner_id;
13706
13707 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13708
13709 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13710 $install_ids[ $slug ] = $install_info['install']->id;
13711 }
13712
13713 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13714 }
13715
13716 $api = $fs->get_api_site_scope();
13717
13718 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13719
13720 if ( ! FS_Api::is_api_error( $result ) ) {
13721 $install = $result;
13722
13723 $fs->reconnect_locally( $has_valid_blog_id );
13724
13725 if (
13726 $change_owner &&
13727 // If successful ownership change.
13728 $fs->get_user()->id != $install->user_id
13729 ) {
13730 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13731 }
13732 }
13733 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13734 $result = $fs->activate_license_on_site( $user, $license_key );
13735 }
13736 }
13737
13738 $is_connected = null;
13739
13740 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
13741 if ( FS_Api::is_blocked( $result ) ) {
13742 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
13743
13744 $is_connected = false;
13745 }
13746
13747 $error = FS_Api::is_api_error_object( $result ) ?
13748 $result->error->message :
13749 var_export( $result, true );
13750 } else {
13751 $is_connected = true;
13752
13753 $fs->network_upgrade_mode_completed();
13754
13755 $fs->_user = $user;
13756
13757 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13758 $fs->_site = $fs->get_network_install();
13759 }
13760
13761 $fs->_sync_license( true, $has_valid_blog_id );
13762
13763 $this->maybe_sync_install_user();
13764
13765 $next_page = $fs->is_addon() ?
13766 $fs->get_parent_instance()->get_account_url() :
13767 $fs->get_after_activation_url( 'after_connect_url' );
13768 }
13769
13770 $fs->update_connectivity_info( $is_connected );
13771 } else {
13772 $next_page = $fs->opt_in(
13773 false,
13774 false,
13775 false,
13776 $license_key,
13777 false,
13778 false,
13779 false,
13780 $is_marketing_allowed,
13781 $sites
13782 );
13783
13784 if ( isset( $next_page->error ) ) {
13785 $error = $next_page->error;
13786 } else {
13787 if ( $is_network_activation_or_migration ) {
13788 /**
13789 * Get the list of sites that were just opted-in (and license activated).
13790 * This is an optimization for the next part below saving some DB queries.
13791 */
13792 $connected_sites = array();
13793 foreach ( $sites as $site ) {
13794 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13795 $connected_sites[ $site['blog_id'] ] = true;
13796 }
13797 }
13798
13799 $all_sites = self::get_sites();
13800 $pending_blog_ids = array();
13801
13802 /**
13803 * 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.
13804 *
13805 * @author Vova Feldman (@svovaf)
13806 */
13807 foreach ( $all_sites as $site ) {
13808 $blog_id = self::get_site_blog_id( $site );
13809
13810 if ( isset( $connected_sites[ $blog_id ] ) ) {
13811 // Site was just connected.
13812 continue;
13813 }
13814
13815 if ( $fs->is_installed_on_site( $blog_id ) ) {
13816 // Site was already connected before.
13817 continue;
13818 }
13819
13820 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13821 // Site's connection was delegated.
13822 continue;
13823 }
13824
13825 if ( $fs->is_anonymous_site( $blog_id ) ) {
13826 // Site connection was already skipped.
13827 continue;
13828 }
13829
13830 $pending_blog_ids[] = $blog_id;
13831 }
13832
13833 if ( ! empty( $pending_blog_ids ) ) {
13834 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13835 $fs->skip_connection( $pending_blog_ids );
13836 } else {
13837 $fs->delegate_connection( $pending_blog_ids );
13838 }
13839 }
13840 }
13841 }
13842 }
13843
13844 if ( false === $error && true === $fs->_storage->require_license_activation ) {
13845 $fs->_storage->require_license_activation = false;
13846 }
13847
13848 $result = array(
13849 'success' => ( false === $error )
13850 );
13851
13852 if ( false !== $error ) {
13853 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
13854 } else {
13855 if ( $fs->is_addon() || $fs->has_addons() ) {
13856 /**
13857 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
13858 * an updated valid user licenses collection will be fetched from the server which is used to also
13859 * update the account add-ons (add-ons the user has licenses for).
13860 *
13861 * @author Leo Fajardo (@leorw)
13862 * @since 2.2.4
13863 */
13864 $fs->purge_valid_user_licenses_cache();
13865 }
13866
13867 $result['next_page'] = $next_page;
13868 }
13869
13870 return $result;
13871 }
13872
13873 /**
13874 * @author Leo Fajardo (@leorw)
13875 * @since 2.3.2
13876 *
13877 * @return array {
13878 * @key string Product slug.
13879 * @value array {
13880 * @property FS_Site $site
13881 * @property FS_Plugin_License $license
13882 * }
13883 * }
13884 */
13885 private function get_parent_and_addons_installs_info() {
13886 $fs = $this->is_addon() ?
13887 $this->get_parent_instance() :
13888 $this;
13889
13890 $installed_addons_ids = array();
13891
13892 $installed_addons_instances = $fs->get_installed_addons();
13893 foreach ( $installed_addons_instances as $instance ) {
13894 $installed_addons_ids[] = $instance->get_id();
13895 }
13896
13897 $addons_ids = array_unique( array_merge(
13898 $installed_addons_ids,
13899 $fs->get_updated_account_addons()
13900 ) );
13901
13902 // Add parent product info.
13903 $installs_info_by_slug_map = array(
13904 $fs->get_slug() => array(
13905 'install' => $fs->get_site(),
13906 'license' => $fs->_get_license()
13907 )
13908 );
13909
13910 foreach ( $addons_ids as $addon_id ) {
13911 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
13912
13913 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
13914
13915 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
13916 // Add-on is not associated with an install entity.
13917 continue;
13918 }
13919
13920 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
13921 'install' => $addon_info['site'],
13922 'license' => isset( $addon_info['license'] ) ?
13923 $addon_info['license'] :
13924 null
13925 );
13926 }
13927
13928 return $installs_info_by_slug_map;
13929 }
13930
13931 /**
13932 * @author Leo Fajardo (@leorw)
13933 * @since 1.2.3.1
13934 */
13935 function _network_activate_ajax_action() {
13936 $this->_logger->entrance();
13937
13938 $this->check_ajax_referer( 'network_activate' );
13939
13940 $plugin_id = fs_request_get( 'module_id', '', 'post' );
13941 $fs = ( $plugin_id == $this->_module_id ) ?
13942 $this :
13943 $this->get_addon_instance( $plugin_id );
13944
13945 $error = false;
13946
13947 $sites = fs_request_get( 'sites', array(), 'post' );
13948 if ( is_array( $sites ) && ! empty( $sites ) ) {
13949 $sites_by_action = array(
13950 'allow' => array(),
13951 'delegate' => array(),
13952 'skip' => array()
13953 );
13954
13955 foreach ( $sites as $site ) {
13956 $sites_by_action[ $site['action'] ][] = $site;
13957 }
13958
13959 $total_sites = count( $sites );
13960 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
13961
13962 $next_page = '';
13963
13964 $has_any_install = fs_request_get_bool( 'has_any_install' );
13965
13966 if ( $total_sites === $total_sites_to_delegate &&
13967 ! $this->is_network_upgrade_mode() &&
13968 ! $has_any_install
13969 ) {
13970 $this->delegate_connection();
13971 } else {
13972 if ( ! empty( $sites_by_action['delegate'] ) ) {
13973 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
13974 }
13975
13976 if ( ! empty( $sites_by_action['skip'] ) ) {
13977 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
13978 }
13979
13980 if ( empty( $sites_by_action['allow'] ) ) {
13981 if ( $has_any_install ) {
13982 $first_install = $fs->find_first_install();
13983
13984 if ( ! is_null( $first_install ) ) {
13985 $fs->_site = $first_install['install'];
13986 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
13987
13988 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
13989 $fs->_storage->network_user_id = $fs->_user->id;
13990 }
13991 }
13992 } else {
13993 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
13994 $next_page = $fs->opt_in(
13995 false,
13996 false,
13997 false,
13998 false,
13999 false,
14000 false,
14001 false,
14002 fs_request_get_bool( 'is_marketing_allowed', null ),
14003 $sites_by_action['allow']
14004 );
14005 } else {
14006 $next_page = $fs->install_with_user(
14007 $this->get_network_user(),
14008 false,
14009 false,
14010 false,
14011 true,
14012 $sites_by_action['allow']
14013 );
14014 }
14015
14016 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14017 $error = $next_page->error;
14018 }
14019 }
14020 }
14021
14022 if ( empty( $next_page ) ) {
14023 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14024 }
14025 } else {
14026 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14027 }
14028
14029 $result = array(
14030 'success' => ( false === $error )
14031 );
14032
14033 if ( false !== $error ) {
14034 $result['error'] = $error;
14035 } else {
14036 $result['next_page'] = $next_page;
14037 }
14038
14039 echo json_encode( $result );
14040
14041 exit;
14042 }
14043
14044 /**
14045 * Billing update AJAX callback.
14046 *
14047 * @author Vova Feldman (@svovaf)
14048 * @since 1.2.1.5
14049 */
14050 function _update_billing_ajax_action() {
14051 $this->_logger->entrance();
14052
14053 $this->check_ajax_referer( 'update_billing' );
14054
14055 if ( ! $this->is_user_admin() ) {
14056 // Only for admins.
14057 self::shoot_ajax_failure();
14058 }
14059
14060 $billing = fs_request_get( 'billing' );
14061
14062 $api = $this->get_api_user_scope();
14063 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14064 'plugin_id' => $this->get_parent_id(),
14065 ) ) );
14066
14067 if ( ! $this->is_api_result_entity( $result ) ) {
14068 self::shoot_ajax_failure();
14069 }
14070
14071 // Purge cached billing.
14072 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14073
14074 self::shoot_ajax_success();
14075 }
14076
14077 /**
14078 * Trial start for anonymous users (AJAX callback).
14079 *
14080 * @author Vova Feldman (@svovaf)
14081 * @since 1.2.1.5
14082 */
14083 function _start_trial_ajax_action() {
14084 $this->_logger->entrance();
14085
14086 $this->check_ajax_referer( 'start_trial' );
14087
14088 if ( ! $this->is_user_admin() ) {
14089 // Only for admins.
14090 self::shoot_ajax_failure();
14091 }
14092
14093 $trial_data = fs_request_get( 'trial' );
14094
14095 $next_page = $this->opt_in(
14096 false,
14097 false,
14098 false,
14099 false,
14100 false,
14101 $trial_data['plan_id']
14102 );
14103
14104 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14105 self::shoot_ajax_failure(
14106 isset( $next_page->error ) ?
14107 $next_page->error->message :
14108 var_export( $next_page, true )
14109 );
14110 }
14111
14112 $this->shoot_ajax_success( array(
14113 'next_page' => $next_page,
14114 ) );
14115 }
14116
14117 /**
14118 * @author Leo Fajardo (@leorw)
14119 * @since 1.2.0
14120 */
14121 function _resend_license_key_ajax_action() {
14122 $this->_logger->entrance();
14123
14124 $this->check_ajax_referer( 'resend_license_key' );
14125
14126 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14127
14128 if ( empty( $email_address ) ) {
14129 exit;
14130 }
14131
14132 $error = false;
14133
14134 $api = $this->get_api_plugin_scope();
14135 $result = $api->call( '/licenses/resend.json', 'post',
14136 array(
14137 'email' => $email_address,
14138 'url' => home_url(),
14139 )
14140 );
14141
14142 if ( is_object( $result ) && isset( $result->error ) ) {
14143 $error = $result->error;
14144
14145 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14146 $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' );
14147 } else if ( 'no_license' === $error->code ) {
14148 $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' );
14149 } else {
14150 $error = $error->message;
14151 }
14152 }
14153
14154 $licenses = array(
14155 'success' => ( false === $error )
14156 );
14157
14158 if ( false !== $error ) {
14159 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14160 }
14161
14162 echo json_encode( $licenses );
14163
14164 exit;
14165 }
14166
14167 /**
14168 * @author Vova Feldman (@svovaf)
14169 * @since 1.2.1.8
14170 *
14171 * @var string
14172 */
14173 private static $_pagenow;
14174
14175 /**
14176 * Get current page or the referer if executing a WP AJAX request.
14177 *
14178 * @author Vova Feldman (@svovaf)
14179 * @since 1.2.1.8
14180 *
14181 * @return string
14182 */
14183 static function get_current_page() {
14184 if ( ! isset( self::$_pagenow ) ) {
14185 global $pagenow;
14186 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14187 /**
14188 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14189 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14190 *
14191 * @author Leo Fajardo (@leorw)
14192 * @since 2.2.3
14193 */
14194 if ( is_network_admin() ) {
14195 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14196 } else if ( is_user_admin() ) {
14197 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14198 } else {
14199 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14200 }
14201
14202 $pagenow = $self_matches[1];
14203 $pagenow = trim( $pagenow, '/' );
14204 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14205 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14206 $pagenow = 'index.php';
14207 } else {
14208 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14209 $pagenow = strtolower( $self_matches[1] );
14210 if ( '.php' !== substr($pagenow, -4, 4) )
14211 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14212 }
14213 }
14214
14215 self::$_pagenow = $pagenow;
14216
14217 if ( self::is_ajax() &&
14218 'admin-ajax.php' === $pagenow
14219 ) {
14220 $referer = fs_get_raw_referer();
14221
14222 if ( is_string( $referer ) ) {
14223 $parts = explode( '?', $referer );
14224
14225 self::$_pagenow = basename( $parts[0] );
14226 }
14227 }
14228 }
14229
14230 return self::$_pagenow;
14231 }
14232
14233 /**
14234 * Helper method to check if user in the plugins page.
14235 *
14236 * @author Vova Feldman (@svovaf)
14237 * @since 1.2.1.5
14238 *
14239 * @return bool
14240 */
14241 static function is_plugins_page() {
14242 return ( 'plugins.php' === self::get_current_page() );
14243 }
14244
14245 /**
14246 * @author Leo Fajardo (@leorw)
14247 * @since 2.2.3
14248 *
14249 * @return bool
14250 */
14251 static function is_plugin_install_page() {
14252 return ( 'plugin-install.php' === self::get_current_page() );
14253 }
14254
14255 /**
14256 * @author Leo Fajardo (@leorw)
14257 * @since 2.0.2
14258 *
14259 * @return bool
14260 */
14261 static function is_updates_page() {
14262 return ( 'update-core.php' === self::get_current_page() );
14263 }
14264
14265 /**
14266 * Helper method to check if user in the themes page.
14267 *
14268 * @author Vova Feldman (@svovaf)
14269 * @since 1.2.2.6
14270 *
14271 * @return bool
14272 */
14273 static function is_themes_page() {
14274 return ( 'themes.php' === self::get_current_page() );
14275 }
14276
14277 #----------------------------------------------------------------------------------
14278 #region Affiliation
14279 #----------------------------------------------------------------------------------
14280
14281 /**
14282 * @author Leo Fajardo (@leorw)
14283 * @since 1.2.3
14284 *
14285 * @return bool
14286 */
14287 function has_affiliate_program() {
14288 if ( ! is_object( $this->_plugin ) ) {
14289 return false;
14290 }
14291
14292 return $this->_plugin->has_affiliate_program();
14293 }
14294
14295 /**
14296 * Get Plugin ID under which we will track affiliate application.
14297 *
14298 * This could either be the Bundle ID or the main plugin ID.
14299 *
14300 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14301 */
14302 private function get_plugin_id_for_affiliate_terms() {
14303 return $this->has_bundle_context() ?
14304 $this->get_bundle_id() :
14305 $this->_plugin->id;
14306 }
14307
14308 /**
14309 * @author Leo Fajardo (@leorw)
14310 * @since 1.2.4
14311 */
14312 private function fetch_affiliate_terms() {
14313 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14314 /**
14315 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14316 */
14317 $plugins_api = $this->has_bundle_context() ?
14318 $this->get_api_bundle_scope() :
14319 $this->get_api_plugin_scope();
14320
14321 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14322
14323 /**
14324 * At this point, we intentionally don't fallback to the main plugin, because the developer has chosen to use bundle. So it makes sense the affiliate program should be in context to the bundle too.
14325 */
14326 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14327 return;
14328 }
14329
14330 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14331 }
14332 }
14333
14334 /**
14335 * @author Leo Fajardo (@leorw)
14336 * @since 1.2.4
14337 */
14338 private function fetch_affiliate_and_custom_terms() {
14339 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14340 $application_data = $this->_storage->affiliate_application_data;
14341 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14342
14343 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14344
14345 $users_api = $this->get_api_user_scope();
14346 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14347 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14348 if ( ! empty( $result->affiliates ) ) {
14349 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14350
14351 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14352 $application_data['status'] = $affiliate->status;
14353 $this->_storage->affiliate_application_data = $application_data;
14354 }
14355
14356 if ( $affiliate->is_using_custom_terms ) {
14357 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14358 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14359 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14360 }
14361 }
14362
14363 $this->affiliate = $affiliate;
14364 }
14365 }
14366 }
14367 }
14368
14369 /**
14370 * @author Leo Fajardo (@leorw)
14371 * @since 1.2.3
14372 */
14373 private function fetch_affiliate_and_terms() {
14374 $this->_logger->entrance();
14375
14376 $this->fetch_affiliate_terms();
14377 $this->fetch_affiliate_and_custom_terms();
14378 }
14379
14380 /**
14381 * @author Leo Fajardo (@leorw)
14382 * @since 1.2.3
14383 *
14384 * @return FS_Affiliate
14385 */
14386 function get_affiliate() {
14387 return $this->affiliate;
14388 }
14389
14390
14391 /**
14392 * @author Leo Fajardo (@leorw)
14393 * @since 1.2.3
14394 *
14395 * @return FS_AffiliateTerms
14396 */
14397 function get_affiliate_terms() {
14398 return is_object( $this->custom_affiliate_terms ) ?
14399 $this->custom_affiliate_terms :
14400 $this->plugin_affiliate_terms;
14401 }
14402
14403 /**
14404 * @author Leo Fajardo (@leorw)
14405 * @since 1.2.3
14406 */
14407 function _submit_affiliate_application() {
14408 $this->_logger->entrance();
14409
14410 $this->check_ajax_referer( 'submit_affiliate_application' );
14411
14412 if ( ! $this->is_user_admin() ) {
14413 // Only for admins.
14414 self::shoot_ajax_failure();
14415 }
14416
14417 $affiliate = fs_request_get( 'affiliate' );
14418
14419 if ( empty( $affiliate['promotion_methods'] ) ) {
14420 unset( $affiliate['promotion_methods'] );
14421 }
14422
14423 if ( ! empty( $affiliate['additional_domains'] ) ) {
14424 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14425 }
14426
14427 if ( ! $this->is_registered() ) {
14428 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14429
14430 if ( ! is_email( $email_address ) ) {
14431 self::shoot_ajax_failure('Invalid email address.');
14432 }
14433
14434 // Opt in but don't track usage.
14435 $next_page = $this->opt_in(
14436 $email_address,
14437 false,
14438 false,
14439 false,
14440 false,
14441 false,
14442 true
14443 );
14444
14445 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14446 self::shoot_ajax_failure(
14447 isset( $next_page->error ) ?
14448 $next_page->error->message :
14449 var_export( $next_page, true )
14450 );
14451 } else if ( $this->is_pending_activation() ) {
14452 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation. Please check your email and click the link to activate your account and then submit the affiliate form again.', 'account-is-pending-activation' ) );
14453 }
14454 }
14455
14456 $this->fetch_affiliate_terms();
14457
14458 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14459
14460 $api = $this->get_api_user_scope();
14461 $result = $api->call(
14462 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14463 'post',
14464 $affiliate
14465 );
14466
14467 if ( $this->is_api_error( $result ) ) {
14468 self::shoot_ajax_failure(
14469 isset( $result->error ) ?
14470 $result->error->message :
14471 var_export( $result, true )
14472 );
14473 } else {
14474 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14475 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14476 }
14477
14478 $affiliate_application_data = array(
14479 'status' => 'pending',
14480 'stats_description' => $affiliate['stats_description'],
14481 'promotion_method_description' => $affiliate['promotion_method_description'],
14482 );
14483
14484 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14485 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14486 }
14487
14488 if ( ! empty( $affiliate['domain'] ) ) {
14489 $affiliate_application_data['domain'] = $affiliate['domain'];
14490 }
14491
14492 if ( ! empty( $affiliate['additional_domains'] ) ) {
14493 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14494 }
14495
14496 $this->_storage->affiliate_application_data = $affiliate_application_data;
14497 }
14498
14499 // Purge cached affiliate.
14500 $api->purge_cache( 'affiliate.json' );
14501
14502 self::shoot_ajax_success( $result );
14503 }
14504
14505 /**
14506 * @author Leo Fajardo (@leorw)
14507 * @since 1.2.3
14508 *
14509 * @return array|null
14510 */
14511 function get_affiliate_application_data() {
14512 if ( empty( $this->_storage->affiliate_application_data ) ) {
14513 return null;
14514 }
14515
14516 return $this->_storage->affiliate_application_data;
14517 }
14518
14519 #endregion Affiliation ------------------------------------------------------------
14520
14521 #----------------------------------------------------------------------------------
14522 #region URL Generators
14523 #----------------------------------------------------------------------------------
14524
14525 /**
14526 * Alias to pricing_url().
14527 *
14528 * @author Vova Feldman (@svovaf)
14529 * @since 1.0.2
14530 *
14531 * @uses pricing_url()
14532 *
14533 * @param string $period Billing cycle
14534 * @param bool $is_trial
14535 *
14536 * @return string
14537 */
14538 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14539 return $this->pricing_url( $period, $is_trial );
14540 }
14541
14542 /**
14543 * @author Vova Feldman (@svovaf)
14544 * @since 1.0.9
14545 *
14546 * @uses get_upgrade_url()
14547 *
14548 * @return string
14549 */
14550 function get_trial_url() {
14551 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14552 }
14553
14554 /**
14555 * @author Leo Fajardo (@leorw)
14556 * @since 2.1.4
14557 *
14558 * @param string $new_version
14559 *
14560 * @return string
14561 */
14562 function version_upgrade_checkout_link( $new_version ) {
14563 if ( ! is_object( $this->_license ) ) {
14564 $url = $this->pricing_url();
14565
14566 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14567 } else {
14568 $subscription = $this->_get_subscription( $this->_license->id );
14569
14570 $url = $this->checkout_url(
14571 is_object( $subscription ) ?
14572 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14573 WP_FS__PERIOD_LIFETIME,
14574 false,
14575 array( 'licenses' => $this->_license->quota )
14576 );
14577
14578 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14579 }
14580
14581 return sprintf(
14582 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14583 sprintf(
14584 '<a href="%s">%s</a>',
14585 $this->apply_filters( 'update_notice_checkout_url', $url ),
14586 $purchase_license_text
14587 ),
14588 $new_version
14589 );
14590 }
14591
14592 /**
14593 * Plugin's pricing URL.
14594 *
14595 * @author Vova Feldman (@svovaf)
14596 * @since 1.0.4
14597 *
14598 * @param string $billing_cycle Billing cycle
14599 *
14600 * @param bool $is_trial
14601 *
14602 * @return string
14603 */
14604 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14605 $this->_logger->entrance();
14606
14607 $params = array(
14608 'billing_cycle' => $billing_cycle
14609 );
14610
14611 if ( $is_trial ) {
14612 $params['trial'] = 'true';
14613 }
14614
14615 $url = $this->is_addon() ?
14616 $this->_parent->addon_url( $this->_slug ) :
14617 $this->_get_admin_page_url( 'pricing', $params );
14618
14619 return $this->get_pricing_url_with_filter( $url );
14620 }
14621
14622 /**
14623 * Retrieves the filtered pricing URL.
14624 *
14625 * @author Leo Fajardo (@leorw)
14626 * @since 2.7.4
14627 *
14628 * @param string $url
14629 *
14630 * @return string
14631 */
14632 private function get_pricing_url_with_filter( $url ) {
14633 return $this->apply_filters( 'pricing_url', $url );
14634 }
14635
14636 /**
14637 * Checkout page URL.
14638 *
14639 * @author Vova Feldman (@svovaf)
14640 * @since 1.0.6
14641 *
14642 * @param string $billing_cycle Billing cycle
14643 * @param bool $is_trial
14644 * @param array $extra (optional) Extra parameters, override other query params.
14645 * @param bool|null $network
14646 *
14647 * @return string
14648 */
14649 function checkout_url(
14650 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14651 $is_trial = false,
14652 $extra = array(),
14653 $network = null
14654 ) {
14655 $this->_logger->entrance();
14656
14657 $params = array(
14658 'checkout' => 'true',
14659 'billing_cycle' => $billing_cycle,
14660 );
14661
14662 if ( $is_trial ) {
14663 $params['trial'] = 'true';
14664 }
14665
14666 /**
14667 * Params in extra override other params.
14668 */
14669 $params = array_merge( $params, $extra );
14670
14671 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
14672 }
14673
14674 /**
14675 * Add-on checkout URL.
14676 *
14677 * @author Vova Feldman (@svovaf)
14678 * @since 1.1.7
14679 *
14680 * @param number $addon_id
14681 * @param number $pricing_id
14682 * @param string $billing_cycle
14683 * @param bool $is_trial
14684 * @param bool|null $network
14685 *
14686 * @return string
14687 */
14688 function addon_checkout_url(
14689 $addon_id,
14690 $pricing_id,
14691 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14692 $is_trial = false,
14693 $network = null
14694 ) {
14695 return $this->checkout_url( $billing_cycle, $is_trial, array(
14696 'plugin_id' => $addon_id,
14697 'pricing_id' => $pricing_id,
14698 ), $network );
14699 }
14700
14701 #endregion
14702
14703 #endregion ------------------------------------------------------------------
14704
14705 /**
14706 * Check if plugin has any add-ons.
14707 *
14708 * @author Vova Feldman (@svovaf)
14709 * @since 1.0.5
14710 *
14711 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14712 *
14713 * @return bool
14714 */
14715 function has_addons() {
14716 $this->_logger->entrance();
14717
14718 return $this->_has_addons;
14719 }
14720
14721 /**
14722 * Check if plugin can work in anonymous mode.
14723 *
14724 * @author Vova Feldman (@svovaf)
14725 * @since 1.0.9
14726 *
14727 * @return bool
14728 *
14729 * @deprecated Please use is_enable_anonymous() instead.
14730 */
14731 function enable_anonymous() {
14732 return $this->_enable_anonymous;
14733 }
14734
14735 /**
14736 * Check if plugin can work in anonymous mode.
14737 *
14738 * @author Vova Feldman (@svovaf)
14739 * @since 1.1.9
14740 *
14741 * @return bool
14742 */
14743 function is_enable_anonymous() {
14744 return $this->_enable_anonymous;
14745 }
14746
14747 /**
14748 * Check if plugin is premium only (no free plans).
14749 *
14750 * @author Vova Feldman (@svovaf)
14751 * @since 1.1.9
14752 *
14753 * @return bool
14754 */
14755 function is_only_premium() {
14756 return $this->_is_premium_only;
14757 }
14758
14759 /**
14760 * Checks if the plugin's type is "plugin". The other type is "theme".
14761 *
14762 * @author Leo Fajardo (@leorw)
14763 * @since 1.2.2
14764 *
14765 * @return bool
14766 */
14767 function is_plugin() {
14768 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14769 }
14770
14771 /**
14772 * @author Leo Fajardo (@leorw)
14773 * @since 1.2.2
14774 *
14775 * @return string
14776 */
14777 function get_module_type() {
14778 if ( ! isset( $this->_module_type ) ) {
14779 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14780 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14781 }
14782
14783 return $this->_module_type;
14784 }
14785
14786 /**
14787 * @author Leo Fajardo (@leorw)
14788 * @since 1.2.2
14789 *
14790 * @return string
14791 */
14792 function get_plugin_main_file_path() {
14793 return $this->_plugin_main_file_path;
14794 }
14795
14796 /**
14797 * Check if module has a premium code version.
14798 *
14799 * Serviceware module might be freemium without any
14800 * premium code version, where the paid features
14801 * are all part of the service.
14802 *
14803 * @author Vova Feldman (@svovaf)
14804 * @since 1.2.1.6
14805 *
14806 * @return bool
14807 */
14808 function has_premium_version() {
14809 return $this->_has_premium_version;
14810 }
14811
14812 /**
14813 * Check if feature supported with current site's plan.
14814 *
14815 * @author Vova Feldman (@svovaf)
14816 * @since 1.0.1
14817 *
14818 * @todo IMPLEMENT
14819 *
14820 * @param number $feature_id
14821 *
14822 * @throws Exception
14823 */
14824 function is_feature_supported( $feature_id ) {
14825 throw new Exception( 'not implemented' );
14826 }
14827
14828 /**
14829 * @author Vova Feldman (@svovaf)
14830 * @since 1.0.1
14831 *
14832 * @return bool Is running in SSL/HTTPS
14833 */
14834 function is_ssl() {
14835 return WP_FS__IS_HTTPS;
14836 }
14837
14838 /**
14839 * @author Vova Feldman (@svovaf)
14840 * @since 1.0.9
14841 *
14842 * @return bool Is running in AJAX call.
14843 *
14844 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
14845 */
14846 static function is_ajax() {
14847 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
14848 }
14849
14850 /**
14851 * Check if it's an AJAX call targeted for the current module.
14852 *
14853 * @author Vova Feldman (@svovaf)
14854 * @since 1.2.0
14855 *
14856 * @param array|string $actions Collection of AJAX actions.
14857 *
14858 * @return bool
14859 */
14860 function is_ajax_action( $actions ) {
14861 // Verify it's an ajax call.
14862 if ( ! self::is_ajax() ) {
14863 return false;
14864 }
14865
14866 // Verify the call is relevant for the plugin.
14867 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
14868 return false;
14869 }
14870
14871 // Verify it's one of the specified actions.
14872 if ( is_string( $actions ) ) {
14873 $actions = explode( ',', $actions );
14874 }
14875
14876 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14877 $ajax_action = fs_request_get( 'action' );
14878
14879 foreach ( $actions as $action ) {
14880 if ( $ajax_action === $this->get_action_tag( $action ) ) {
14881 return true;
14882 }
14883 }
14884 }
14885
14886 return false;
14887 }
14888
14889 /**
14890 * Check if it's an AJAX call targeted for current request.
14891 *
14892 * @author Vova Feldman (@svovaf)
14893 * @since 1.2.0
14894 *
14895 * @param array|string $actions Collection of AJAX actions.
14896 * @param number|null $module_id
14897 *
14898 * @return bool
14899 */
14900 static function is_ajax_action_static( $actions, $module_id = null ) {
14901 // Verify it's an ajax call.
14902 if ( ! self::is_ajax() ) {
14903 return false;
14904 }
14905
14906
14907 if ( ! empty( $module_id ) ) {
14908 // Verify the call is relevant for the plugin.
14909 if ( $module_id != fs_request_get( 'module_id' ) ) {
14910 return false;
14911 }
14912 }
14913
14914 // Verify it's one of the specified actions.
14915 if ( is_string( $actions ) ) {
14916 $actions = explode( ',', $actions );
14917 }
14918
14919 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14920 $ajax_action = fs_request_get( 'action' );
14921
14922 foreach ( $actions as $action ) {
14923 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
14924 return true;
14925 }
14926 }
14927 }
14928
14929 return false;
14930 }
14931
14932 /**
14933 * @author Vova Feldman (@svovaf)
14934 * @since 1.1.7
14935 *
14936 * @return bool
14937 */
14938 static function is_cron() {
14939 return ( defined( 'DOING_CRON' ) && DOING_CRON );
14940 }
14941
14942 /**
14943 * @author Leo Fajardo (@leorw)
14944 * @since 2.5.0
14945 *
14946 * @return bool
14947 */
14948 static function is_admin_post() {
14949 return ( 'admin-post.php' === self::get_current_page() );
14950 }
14951
14952 /**
14953 * Check if a real user is visiting the admin dashboard.
14954 *
14955 * @author Vova Feldman (@svovaf)
14956 * @since 1.1.7
14957 *
14958 * @return bool
14959 */
14960 function is_user_in_admin() {
14961 return (
14962 is_admin() &&
14963 ! self::is_ajax() &&
14964 ! self::is_cron() &&
14965 ! self::is_admin_post()
14966 );
14967 }
14968
14969 /**
14970 * Check if a real user is in the customizer view.
14971 *
14972 * @author Vova Feldman (@svovaf)
14973 * @since 1.2.2.7
14974 *
14975 * @return bool
14976 */
14977 static function is_customizer() {
14978 return is_customize_preview();
14979 }
14980
14981 /**
14982 * Check if running in HTTPS and if site's plan matching the specified plan.
14983 *
14984 * @param string $plan
14985 * @param bool $exact
14986 *
14987 * @return bool
14988 */
14989 function is_ssl_and_plan( $plan, $exact = false ) {
14990 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
14991 }
14992
14993 /**
14994 * Construct plugin's settings page URL.
14995 *
14996 * @author Vova Feldman (@svovaf)
14997 * @since 1.0.4
14998 *
14999 * @param string $page
15000 * @param array $params
15001 * @param bool|null $network
15002 *
15003 * @return string
15004 */
15005 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15006 if ( is_null( $network ) ) {
15007 $network = (
15008 $this->_is_network_active &&
15009 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15010 );
15011 }
15012
15013 if ( 0 < count( $params ) ) {
15014 foreach ( $params as $k => $v ) {
15015 $params[ $k ] = urlencode( $v );
15016 }
15017 }
15018
15019 $page_param = $this->_menu->get_slug( $page );
15020
15021 if ( empty( $page ) &&
15022 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15023 $this->show_opt_in_on_themes_page()
15024 ) {
15025 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15026
15027 return add_query_arg(
15028 $params,
15029 $this->admin_url( 'themes.php', 'admin', $network )
15030 );
15031 }
15032
15033 if ( ! $this->has_settings_menu() ) {
15034 if ( ! empty( $page ) ) {
15035 // Module doesn't have a setting page, but since the request is for
15036 // a specific Freemius page, use the admin.php path.
15037 return add_query_arg( array_merge( $params, array(
15038 'page' => $page_param,
15039 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15040 } else {
15041 if ( $this->is_activation_mode() ) {
15042 /**
15043 * @author Vova Feldman
15044 * @since 1.2.1.6
15045 *
15046 * If plugin doesn't have a settings page, create one for the opt-in screen.
15047 */
15048 return add_query_arg( array_merge( $params, array(
15049 'page' => $this->_slug,
15050 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15051 } else {
15052 // Plugin without a settings page.
15053 return add_query_arg(
15054 $params,
15055 $this->admin_url( 'plugins.php', 'admin', $network )
15056 );
15057 }
15058 }
15059 }
15060
15061 // Module has a submenu settings page.
15062 if ( ! $this->_menu->is_top_level() ) {
15063 $parent_slug = $this->_menu->get_parent_slug();
15064 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15065 $parent_slug :
15066 'admin.php';
15067
15068 return add_query_arg( array_merge( $params, array(
15069 'page' => $page_param,
15070 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15071 }
15072
15073 // Module has a top level CPT settings page.
15074 if ( $this->_menu->is_cpt() ) {
15075 if ( empty( $page ) && $this->is_activation_mode() ) {
15076 return add_query_arg( array_merge( $params, array(
15077 'page' => $page_param
15078 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15079 } else {
15080 if ( ! empty( $page ) ) {
15081 $params['page'] = $page_param;
15082 }
15083
15084 return add_query_arg(
15085 $params,
15086 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15087 );
15088 }
15089 }
15090
15091 // Module has a custom top level settings page.
15092 return add_query_arg( array_merge( $params, array(
15093 'page' => $page_param,
15094 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15095 }
15096
15097 #--------------------------------------------------------------------------------
15098 #region Multisite
15099 #--------------------------------------------------------------------------------
15100
15101 /**
15102 * @author Leo Fajardo (@leorw)
15103 * @since 2.0.0
15104 *
15105 * @return bool
15106 */
15107 function is_network_active() {
15108 return $this->_is_network_active;
15109 }
15110
15111 /**
15112 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15113 *
15114 * @author Leo Fajardo (@leorw)
15115 * @since 2.0.0
15116 *
15117 * @param bool|int[] $all_or_blog_ids
15118 */
15119 private function delegate_connection( $all_or_blog_ids = true ) {
15120 $this->_logger->entrance();
15121
15122 $this->_admin_notices->remove_sticky( 'connect_account' );
15123
15124 if ( true === $all_or_blog_ids ) {
15125 // All sites delegation.
15126 $this->_storage->store( 'is_delegated_connection', true, true );
15127 } else {
15128 // Specified sites delegation.
15129 foreach ( $all_or_blog_ids as $blog_id ) {
15130 $this->delegate_site_connection( $blog_id );
15131 }
15132 }
15133
15134 $this->network_upgrade_mode_completed();
15135 }
15136
15137 /**
15138 * Delegate specific network site conncetion to the site admin.
15139 *
15140 * @author Vova Feldman (@svovaf)
15141 * @since 2.0.0
15142 *
15143 * @param int $blog_id
15144 */
15145 private function delegate_site_connection( $blog_id ) {
15146 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15147 }
15148
15149 /**
15150 * Check if super-admin delegated the connection of ALL sites to the site admins.
15151 *
15152 * @author Vova Feldman (@svovaf)
15153 * @since 2.0.0
15154 *
15155 * @return bool
15156 */
15157 function is_network_delegated_connection() {
15158 if ( ! $this->_is_network_active ) {
15159 return false;
15160 }
15161
15162 return $this->_storage->get( 'is_delegated_connection', false, true );
15163 }
15164
15165 /**
15166 * @author Leo Fajardo (@leorw)
15167 * @since 2.0.0
15168 *
15169 * @param int $blog_id
15170 *
15171 * @return bool
15172 */
15173 function is_site_delegated_connection( $blog_id = 0 ) {
15174 if ( ! $this->_is_network_active ) {
15175 return false;
15176 }
15177
15178 if ( 0 == $blog_id ) {
15179 $blog_id = get_current_blog_id();
15180 }
15181
15182 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15183 }
15184
15185 /**
15186 * Check if delegated the connection. When running within the network admin,
15187 * and haven't specified the blog ID, checks if network level delegated. If running
15188 * within a site admin or specified a blog ID, check if delegated the connection for
15189 * the current context site.
15190 *
15191 * If executed outside the the admin, check if delegated the connection
15192 * for the current context site OR the whole network.
15193 *
15194 * @author Vova Feldman (@svovaf)
15195 * @since 2.0.0
15196 *
15197 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15198 *
15199 * @return bool
15200 */
15201 function is_delegated_connection( $blog_id = 0 ) {
15202 if ( ! $this->_is_network_active ) {
15203 return false;
15204 }
15205
15206 if ( fs_is_network_admin() && 0 == $blog_id ) {
15207 return $this->is_network_delegated_connection();
15208 }
15209
15210 return (
15211 $this->is_network_delegated_connection() ||
15212 $this->is_site_delegated_connection( $blog_id )
15213 );
15214 }
15215
15216 /**
15217 * Check if the current module is active for the site.
15218 *
15219 * @author Vova Feldman (@svovaf)
15220 * @since 2.0.0
15221 *
15222 * @param int $blog_id
15223 *
15224 * @return bool
15225 */
15226 function is_active_for_site( $blog_id ) {
15227 if ( ! is_multisite() ) {
15228 // Not a multisite and this code is executed, means that the plugin is active.
15229 return true;
15230 }
15231
15232 if ( $this->is_theme() ) {
15233 // All themes are site level activated.
15234 return true;
15235 }
15236
15237 if ( $this->_is_network_active ) {
15238 // Plugin was network activated so it's active.
15239 return true;
15240 }
15241
15242 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15243 }
15244
15245 /**
15246 * @todo Implement pagination when accessing the subsites collection.
15247 *
15248 * @author Leo Fajardo (@leorw)
15249 * @since 2.0.0
15250 *
15251 * @param int $limit Default to 1,000
15252 * @param int $offset Default to 0
15253 *
15254 * @return array Active & public sites collection.
15255 */
15256 static function get_sites( $limit = 1000, $offset = 0 ) {
15257 if ( ! is_multisite() ) {
15258 return array();
15259 }
15260
15261 /**
15262 * For consistency with get_blog_list() which only return active public sites.
15263 *
15264 * @author Vova Feldman (@svovaf)
15265 */
15266 $args = array(
15267 /**
15268 * Commented out in order to handle the migration of site options whether the site is public or not.
15269 *
15270 * @author Leo Fajardo (@leorw)
15271 * @since 2.2.1
15272 */
15273 // 'public' => 1,
15274 'archived' => 0,
15275 'mature' => 0,
15276 'spam' => 0,
15277 'deleted' => 0,
15278 'number' => $limit,
15279 'offset' => $offset,
15280 );
15281
15282 return get_sites( $args );
15283 }
15284
15285 /**
15286 * Checks if a given blog is active.
15287 *
15288 * @author Vova Feldman (@svovaf)
15289 * @since 2.0.0
15290 *
15291 * @param $blog_id
15292 *
15293 * @return bool
15294 */
15295 private static function is_site_active( $blog_id ) {
15296 global $wpdb;
15297
15298 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15299
15300 if ( ! is_object( $blog_info ) ) {
15301 return false;
15302 }
15303
15304 return (
15305 true == $blog_info->public &&
15306 false == $blog_info->archived &&
15307 false == $blog_info->mature &&
15308 false == $blog_info->spam &&
15309 false == $blog_info->deleted
15310 );
15311 }
15312
15313 /**
15314 * Get a mapping between the site addresses to their blog IDs.
15315 *
15316 * @author Vova Feldman (@svovaf)
15317 * @since 2.0.0
15318 *
15319 * @return array {
15320 * @key string Site address without protocol with a trailing slash.
15321 * @value int Site's blog ID.
15322 * }
15323 */
15324 private function get_address_to_blog_map() {
15325 $sites = self::get_sites();
15326
15327 // Map site addresses to their blog IDs.
15328 $address_to_blog_map = array();
15329 foreach ( $sites as $site ) {
15330 $blog_id = self::get_site_blog_id( $site );
15331 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15332 $address_to_blog_map[ $address ] = $blog_id;
15333 }
15334
15335 return $address_to_blog_map;
15336 }
15337
15338 /**
15339 * Get a mapping between the site addresses to their blog IDs.
15340 *
15341 * @author Vova Feldman (@svovaf)
15342 * @since 2.0.0
15343 *
15344 * @return array {
15345 * @key int Site's blog ID.
15346 * @value FS_Site Associated install.
15347 * }
15348 */
15349 function get_blog_install_map() {
15350 $sites = self::get_sites();
15351
15352 // Map site blog ID to its install.
15353 $install_map = array();
15354
15355 foreach ( $sites as $site ) {
15356 $blog_id = self::get_site_blog_id( $site );
15357 $install = $this->get_install_by_blog_id( $blog_id );
15358
15359 if ( is_object( $install ) ) {
15360 $install_map[ $blog_id ] = $install;
15361 }
15362 }
15363
15364 return $install_map;
15365 }
15366
15367 /**
15368 * @author Vova Feldman (@svovaf)
15369 * @since 2.5.1
15370 *
15371 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15372 *
15373 * @return int[]
15374 */
15375 private function get_blog_ids( $is_delegated = null ) {
15376 $blog_ids = array();
15377
15378 $sites = self::get_sites();
15379 foreach ( $sites as $site ) {
15380 $blog_id = self::get_site_blog_id( $site );
15381
15382 if (
15383 is_null( $is_delegated ) ||
15384 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15385 ) {
15386 $blog_ids[] = $blog_id;
15387 }
15388 }
15389
15390 return $blog_ids;
15391 }
15392
15393 /**
15394 * @author Vova Feldman (@svovaf)
15395 * @since 2.5.1
15396 *
15397 * @return int[]
15398 */
15399 private function get_non_delegated_blog_ids() {
15400 return $this->get_blog_ids( false );
15401 }
15402
15403 /**
15404 * Gets a map of module IDs that the given user has opted-in to.
15405 *
15406 * @author Leo Fajardo (@leorw)
15407 * @since 2.1.0
15408 *
15409 * @param number $fs_user_id
15410 *
15411 * @return array {
15412 * @key number $plugin_id
15413 * @value bool Always true.
15414 * }
15415 */
15416 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15417 self::$_static_logger->entrance();
15418
15419 if ( ! is_multisite() ) {
15420 $installs = array_merge(
15421 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15422 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15423 );
15424 } else {
15425 $sites = self::get_sites();
15426
15427 $installs = array();
15428 foreach ( $sites as $site ) {
15429 $blog_id = self::get_site_blog_id( $site );
15430
15431 $installs = array_merge(
15432 $installs,
15433 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15434 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15435 );
15436 }
15437 }
15438
15439 $module_ids_map = array();
15440 foreach ( $installs as $install ) {
15441 if ( is_object( $install ) &&
15442 FS_Site::is_valid_id( $install->id ) &&
15443 FS_User::is_valid_id( $install->user_id ) &&
15444 ( $install->user_id == $fs_user_id )
15445 ) {
15446 $module_ids_map[ $install->plugin_id ] = true;
15447 }
15448 }
15449
15450 return $module_ids_map;
15451 }
15452
15453 /**
15454 * @author Leo Fajardo (@leorw)
15455 *
15456 * @return null|array {
15457 * 'install' => FS_Site Module's install,
15458 * 'blog_id' => string The associated blog ID.
15459 * }
15460 */
15461 function find_first_install() {
15462 $sites = self::get_sites();
15463
15464 foreach ( $sites as $site ) {
15465 $blog_id = self::get_site_blog_id( $site );
15466 $install = $this->get_install_by_blog_id( $blog_id );
15467
15468 if ( is_object( $install ) ) {
15469 return array(
15470 'install' => $install,
15471 'blog_id' => $blog_id
15472 );
15473 }
15474 }
15475
15476 return null;
15477 }
15478
15479 /**
15480 * Switches the Freemius site level context to a specified blog.
15481 *
15482 * @author Vova Feldman (@svovaf)
15483 * @since 2.0.0
15484 *
15485 * @param int $blog_id
15486 * @param FS_Site $install
15487 * @param bool $flush
15488 *
15489 * @return bool Since 2.3.1 returns if a switch was made.
15490 */
15491 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
15492 if ( ! is_numeric( $blog_id ) ) {
15493 return false;
15494 }
15495
15496 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15497 return false;
15498 }
15499
15500 switch_to_blog( $blog_id );
15501 $this->_context_is_network_or_blog_id = $blog_id;
15502
15503 self::$_accounts->set_site_blog_context( $blog_id );
15504 $this->_storage->set_site_blog_context( $blog_id );
15505 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15506
15507 $this->_site = is_object( $install ) ?
15508 $install :
15509 $this->get_install_by_blog_id( $blog_id );
15510
15511 $this->_user = false;
15512 $this->_licenses = false;
15513 $this->_license = null;
15514 $this->is_whitelabeled = null;
15515
15516 if ( is_object( $this->_site ) ) {
15517 // Try to fetch user from install.
15518 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15519
15520 if ( ! is_object( $this->_user ) &&
15521 FS_User::is_valid_id( $this->_storage->prev_user_id )
15522 ) {
15523 // Try to fetch previously saved user.
15524 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15525
15526 if ( ! is_object( $this->_user ) ) {
15527 // Fallback to network's user.
15528 $this->_user = $this->get_network_user();
15529 }
15530 }
15531
15532 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15533
15534 if ( ! empty( $all_plugin_licenses ) ) {
15535 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15536 $this->_license = null;
15537 } else {
15538 $license_found = false;
15539 foreach ( $all_plugin_licenses as $license ) {
15540 if ( $license->id == $this->_site->license_id ) {
15541 // License found.
15542 $this->_license = $license;
15543 $license_found = true;
15544 break;
15545 }
15546 }
15547
15548 if ( $license_found ) {
15549 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15550 }
15551 }
15552
15553 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15554 }
15555 }
15556
15557 unset( $this->_site_api );
15558 unset( $this->_user_api );
15559
15560 return true;
15561 }
15562
15563 /**
15564 * Restore the blog context to the blog that originally loaded the module.
15565 *
15566 * @author Vova Feldman (@svovaf)
15567 * @since 2.0.0
15568 */
15569 function restore_current_blog() {
15570 $this->switch_to_blog( $this->_blog_id );
15571 }
15572
15573 /**
15574 * @author Vova Feldman (@svovaf)
15575 * @since 2.0.0
15576 *
15577 * @param array|WP_Site $site
15578 *
15579 * @return int
15580 */
15581 static function get_site_blog_id( &$site ) {
15582 return ( $site instanceof WP_Site ) ?
15583 $site->blog_id :
15584 ( is_object( $site ) && isset( $site->userblog_id ) ?
15585 $site->userblog_id :
15586 $site['blog_id'] );
15587 }
15588
15589 /**
15590 * @author Vova Feldman (@svovaf)
15591 * @since 2.5.1
15592 *
15593 * @param WP_Site[]|array[] $sites
15594 *
15595 * @return int[]
15596 */
15597 static function get_sites_blog_ids( $sites ) {
15598 $blog_ids = array();
15599 foreach ( $sites as $site ) {
15600 $blog_ids[] = self::get_site_blog_id( $site );
15601 }
15602
15603 return $blog_ids;
15604 }
15605
15606 /**
15607 * @author Leo Fajardo (@leorw)
15608 * @since 2.0.0
15609 *
15610 * @param array|WP_Site|null $site
15611 * @param bool $load_registration Since 2.5.1 When set to `true` the method will attempt to return the subsite's registration date, regardless of the `$site` type and value. In most calls, the registration date will be returned anyway, even when the value is `false`. This param is purely for performance optimization.
15612 *
15613 * @return array
15614 */
15615 function get_site_info( $site = null, $load_registration = false ) {
15616 $this->_logger->entrance();
15617
15618 $switched = false;
15619
15620 $registration_date = null;
15621
15622 if ( is_null( $site ) ) {
15623 $url = self::get_unfiltered_site_url();
15624 $name = get_bloginfo( 'name' );
15625 $blog_id = null;
15626 } else {
15627 $blog_id = self::get_site_blog_id( $site );
15628
15629 if ( get_current_blog_id() != $blog_id ) {
15630 switch_to_blog( $blog_id );
15631 $switched = true;
15632 }
15633
15634 if ( $site instanceof WP_Site ) {
15635 $url = $site->siteurl;
15636 $name = $site->blogname;
15637 $registration_date = $site->registered;
15638 } else {
15639 $url = self::get_unfiltered_site_url( $blog_id );
15640 $name = get_bloginfo( 'name' );
15641 }
15642 }
15643
15644 if ( empty( $registration_date ) && $load_registration ) {
15645 $blog_details = get_blog_details( $blog_id, false );
15646
15647 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15648 $registration_date = $blog_details->registered;
15649 }
15650 }
15651
15652 $info = array(
15653 'uid' => $this->get_anonymous_id( $blog_id ),
15654 'url' => $url,
15655 );
15656
15657 // Add these diagnostic information only if user allowed to track.
15658 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
15659 $info = array_merge( $info, array(
15660 'title' => $name,
15661 'language' => self::get_sanitized_language(),
15662 ) );
15663 }
15664
15665 if ( is_numeric( $blog_id ) ) {
15666 $info['blog_id'] = $blog_id;
15667 }
15668
15669 if ( ! empty( $registration_date ) ) {
15670 $info[ 'registration_date' ] = $registration_date;
15671 }
15672
15673 if ( $switched ) {
15674 restore_current_blog();
15675 }
15676
15677 return $info;
15678 }
15679
15680 /**
15681 * Load the module's install based on the blog ID.
15682 *
15683 * @author Vova Feldman (@svovaf)
15684 * @since 2.0.0
15685 *
15686 * @param int|null $blog_id
15687 *
15688 * @return FS_Site
15689 */
15690 function get_install_by_blog_id( $blog_id = null ) {
15691 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15692 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15693
15694 if ( is_object( $install ) &&
15695 is_numeric( $install->id ) &&
15696 is_numeric( $install->user_id ) &&
15697 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15698 ) {
15699 // Load site.
15700 $install = clone $install;
15701 }
15702
15703 return $install;
15704 }
15705
15706 /**
15707 * Check if module is installed on a specified site.
15708 *
15709 * @author Vova Feldman (@svovaf)
15710 * @since 2.0.0
15711 *
15712 * @param int|null $blog_id
15713 *
15714 * @return bool
15715 */
15716 function is_installed_on_site( $blog_id = null ) {
15717 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15718 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15719
15720 return (
15721 is_object( $install ) &&
15722 is_numeric( $install->id ) &&
15723 is_numeric( $install->user_id ) &&
15724 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15725 );
15726 }
15727
15728 /**
15729 * Check if super-admin connected at least one site via the network opt-in.
15730 *
15731 * @author Vova Feldman (@svovaf)
15732 * @since 2.0.0
15733 *
15734 * @return bool
15735 */
15736 function is_network_registered() {
15737 if ( ! $this->_is_network_active ) {
15738 return false;
15739 }
15740
15741 return FS_User::is_valid_id( $this->_storage->network_user_id );
15742 }
15743
15744 /**
15745 * Returns the main user associated with the network.
15746 *
15747 * @author Vova Feldman (@svovaf)
15748 * @since 2.0.0
15749 *
15750 * @return FS_User
15751 */
15752 function get_network_user() {
15753 if ( ! $this->_is_network_active ) {
15754 return null;
15755 }
15756
15757 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15758 self::_get_user_by_id( $this->_storage->network_user_id ) :
15759 null;
15760 }
15761
15762 /**
15763 * Returns the current context user or the network's main user.
15764 *
15765 * @author Vova Feldman (@svovaf)
15766 * @since 2.0.0
15767 *
15768 * @return FS_User
15769 */
15770 function get_current_or_network_user() {
15771 return ( $this->_user instanceof FS_User ) ?
15772 $this->_user :
15773 $this->get_network_user();
15774 }
15775
15776 /**
15777 * Returns the main install associated with the network.
15778 *
15779 * @author Vova Feldman (@svovaf)
15780 * @since 2.0.0
15781 *
15782 * @return FS_Site
15783 */
15784 function get_network_install() {
15785 if ( ! $this->_is_network_active ) {
15786 return null;
15787 }
15788
15789 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15790 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15791 null;
15792 }
15793
15794 /**
15795 * Returns the blog ID that is associated with the main install.
15796 *
15797 * @author Leo Fajardo (@leorw)
15798 * @since 2.0.0
15799 *
15800 * @return int|null
15801 */
15802 function get_network_install_blog_id() {
15803 if ( ! $this->_is_network_active ) {
15804 return null;
15805 }
15806
15807 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15808 $this->_storage->network_install_blog_id :
15809 null;
15810 }
15811
15812 /**
15813 * Returns the current context install or the network's main install.
15814 *
15815 * @author Vova Feldman (@svovaf)
15816 * @since 2.0.0
15817 *
15818 * @return FS_Site
15819 */
15820 function get_current_or_network_install() {
15821 return ( $this->_site instanceof FS_Site ) ?
15822 $this->_site :
15823 $this->get_network_install();
15824 }
15825
15826 /**
15827 * Check if executing a site level action from the network level admin.
15828 *
15829 * @author Vova Feldman (@svovaf)
15830 * @since 2.0.0
15831 *
15832 * @return false|int If yes, return the requested blog ID.
15833 */
15834 private function is_network_level_site_specific_action() {
15835 if ( ! $this->_is_network_active ) {
15836 return false;
15837 }
15838
15839 if ( ! fs_is_network_admin() ) {
15840 return false;
15841 }
15842
15843 $blog_id = fs_request_get( 'blog_id', '' );
15844
15845 return is_numeric( $blog_id ) ? $blog_id : false;
15846 }
15847
15848 /**
15849 * Check if executing an action from the network level admin.
15850 *
15851 * @author Vova Feldman (@svovaf)
15852 * @since 2.0.0
15853 *
15854 * @return bool
15855 */
15856 private function is_network_level_action() {
15857 return ( $this->_is_network_active && fs_is_network_admin() );
15858 }
15859
15860 /**
15861 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
15862 * 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.
15863 *
15864 * @author Vova Feldman (@svovaf)
15865 * @since 2.0.0
15866 *
15867 * @param int $context_blog_id
15868 */
15869 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
15870 $this->_logger->entrance();
15871
15872 if ( $this->_is_network_active ) {
15873 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
15874 $installs_map = $this->get_blog_install_map();
15875
15876 foreach ( $installs_map as $blog_id => $install ) {
15877 /**
15878 * @var FS_Site $install
15879 */
15880 if ( $context_blog_id == $blog_id ) {
15881 continue;
15882 }
15883
15884 if ( $install->user_id != $this->_storage->network_user_id ) {
15885 continue;
15886 }
15887
15888 // Switch reference to a blog that is opted-in and belong to the same super-admin.
15889 $this->_storage->network_install_blog_id = $blog_id;
15890 break;
15891 }
15892 }
15893 }
15894
15895 if ( ! $this->is_registered() ) {
15896 return;
15897 }
15898
15899 if ( $this->is_sync_cron_scheduled() &&
15900 $context_blog_id == $this->get_sync_cron_blog_id()
15901 ) {
15902 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
15903 }
15904
15905 if ( $this->is_install_sync_scheduled() &&
15906 $context_blog_id == $this->get_install_sync_cron_blog_id()
15907 ) {
15908 $this->schedule_install_sync( $context_blog_id );
15909 }
15910 }
15911
15912 /**
15913 * Executed after site deactivation, archive, or flag as spam.
15914 *
15915 * @author Vova Feldman (@svovaf)
15916 * @since 2.0.0
15917 *
15918 * @param int $context_blog_id
15919 */
15920 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
15921 $this->_logger->entrance();
15922
15923 $install = $this->get_install_by_blog_id( $context_blog_id );
15924
15925 if ( ! is_object( $install ) ) {
15926 // Site not connected.
15927 return;
15928 }
15929
15930 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15931
15932 if ( ! $this->is_registered() ) {
15933 return;
15934 }
15935
15936 $current_blog_id = get_current_blog_id();
15937
15938 $this->switch_to_blog( $context_blog_id );
15939
15940 // Send deactivation event.
15941 $this->sync_install( array(
15942 'is_active' => false,
15943 ) );
15944
15945 $this->switch_to_blog( $current_blog_id );
15946 }
15947
15948 /**
15949 * Executed after site deletion.
15950 *
15951 * @author Vova Feldman (@svovaf)
15952 * @since 2.0.0
15953 *
15954 * @param int $context_blog_id
15955 * @param bool $drop True if site's database tables should be dropped. Default is false.
15956 */
15957 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
15958 $this->_logger->entrance();
15959
15960 $install = $this->get_install_by_blog_id( $context_blog_id );
15961
15962 if ( ! is_object( $install ) ) {
15963 // Site not connected.
15964 return;
15965 }
15966
15967 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15968
15969 if ( ! $this->is_registered() ) {
15970 return;
15971 }
15972
15973 $current_blog_id = get_current_blog_id();
15974
15975 $this->switch_to_blog( $context_blog_id );
15976
15977 if ( $drop ) {
15978 // Delete install if dropping site DB.
15979 $this->delete_account_event();
15980 } else {
15981 // Send deactivation event.
15982 $this->sync_install( array(
15983 'is_active' => false,
15984 ) );
15985 }
15986
15987 $this->switch_to_blog( $current_blog_id );
15988 }
15989
15990 /**
15991 * Executed after site deletion, called from wp_delete_site
15992 *
15993 * @author Dario Curvino (@dudo)
15994 * @since 2.5.0
15995 *
15996 * @param WP_Site $old_site
15997 */
15998 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
15999 $this->_logger->entrance();
16000
16001 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16002 }
16003
16004 /**
16005 * Executed after site re-activation.
16006 *
16007 * @author Vova Feldman (@svovaf)
16008 * @since 2.0.0
16009 *
16010 * @param int $context_blog_id
16011 */
16012 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16013 $this->_logger->entrance();
16014
16015 $install = $this->get_install_by_blog_id( $context_blog_id );
16016
16017 if ( ! is_object( $install ) ) {
16018 // Site not connected.
16019 return;
16020 }
16021
16022 if ( ! self::is_site_active( $context_blog_id ) ) {
16023 // Site not yet active (can be in spam mode, archived, deleted...).
16024 return;
16025 }
16026
16027 $current_blog_id = get_current_blog_id();
16028
16029 $this->switch_to_blog( $context_blog_id );
16030
16031 // Send re-activation event.
16032 $this->sync_install( array(
16033 'is_active' => true,
16034 ) );
16035
16036 $this->switch_to_blog( $current_blog_id );
16037 }
16038
16039 #endregion Multisite
16040
16041 /**
16042 * @author Leo Fajardo (@leorw)
16043 *
16044 * @param string $path
16045 * @param string $scheme
16046 * @param bool $network
16047 *
16048 * @return string
16049 */
16050 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16051 return ( $this->_is_network_active && $network ) ?
16052 network_admin_url( $path, $scheme ) :
16053 admin_url( $path, $scheme );
16054 }
16055
16056 /**
16057 * Check if currently in a specified admin page.
16058 *
16059 * @author Vova Feldman (@svovaf)
16060 * @since 1.2.2.7
16061 *
16062 * @param string $page
16063 *
16064 * @return bool
16065 */
16066 function is_admin_page( $page ) {
16067 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16068 }
16069
16070 /**
16071 * Check if currently in the product's main admin page.
16072 *
16073 * @author Vova Feldman (@svovaf)
16074 * @since 2.3.1
16075 *
16076 * @return bool
16077 */
16078 function is_main_admin_page() {
16079 return $this->is_admin_page( '' );
16080 }
16081
16082 /**
16083 * Get module's main admin setting page URL.
16084 *
16085 * @author Vova Feldman (@svovaf)
16086 * @since 1.2.2.7
16087 *
16088 * @return string
16089 */
16090 function main_menu_url() {
16091 return $this->_menu->main_menu_url();
16092 }
16093
16094 /**
16095 * Check if currently on the theme's setting page or
16096 * on any of the Freemius added pages (via tabs).
16097 *
16098 * @author Vova Feldman (@svovaf)
16099 * @since 1.2.2.7
16100 *
16101 * @return bool
16102 *
16103 * @deprecated Please use is_product_settings_page() instead;
16104 */
16105 function is_theme_settings_page() {
16106 return $this->is_product_settings_page();
16107 }
16108
16109 /**
16110 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16111 *
16112 * @author Vova Feldman (@svovaf)
16113 * @since 1.2.2.7
16114 *
16115 * @return bool
16116 */
16117 function is_product_settings_page() {
16118 $page = fs_request_get( 'page', '', 'get' );
16119 $menu_slug = $this->_menu->get_slug();
16120
16121 if ( $page === $menu_slug ) {
16122 return true;
16123 }
16124
16125 return fs_starts_with(
16126 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16127 $page,
16128 ( $menu_slug . '-' )
16129 );
16130 }
16131
16132 /**
16133 * Plugin's account page + sync license URL.
16134 *
16135 * @author Vova Feldman (@svovaf)
16136 * @since 1.1.9.1
16137 *
16138 * @param bool|number $plugin_id
16139 * @param bool $add_action_nonce
16140 * @param array $params
16141 *
16142 * @return string
16143 */
16144 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16145 if ( is_numeric( $plugin_id ) ) {
16146 $params['plugin_id'] = $plugin_id;
16147 }
16148
16149 return $this->get_account_url(
16150 $this->get_unique_affix() . '_sync_license',
16151 $params,
16152 $add_action_nonce
16153 );
16154 }
16155
16156 /**
16157 * Plugin's account URL.
16158 *
16159 * @author Vova Feldman (@svovaf)
16160 * @since 1.0.4
16161 *
16162 * @param bool|string $action
16163 * @param array $params
16164 *
16165 * @param bool $add_action_nonce
16166 *
16167 * @return string
16168 */
16169 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16170 if ( is_string( $action ) ) {
16171 $params['fs_action'] = $action;
16172 }
16173
16174 self::require_pluggable_essentials();
16175
16176 return ( $add_action_nonce && is_string( $action ) ) ?
16177 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16178 $this->_get_admin_page_url( 'account', $params );
16179 }
16180
16181 /**
16182 * @author Vova Feldman (@svovaf)
16183 * @since 1.2.0
16184 *
16185 * @param string $tab
16186 * @param bool $action
16187 * @param array $params
16188 * @param bool $add_action_nonce
16189 *
16190 * @return string
16191 *
16192 * @uses get_account_url()
16193 */
16194 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16195 $params['tab'] = $tab;
16196
16197 return $this->get_account_url( $action, $params, $add_action_nonce );
16198 }
16199
16200 /**
16201 * Plugin's account URL.
16202 *
16203 * @author Vova Feldman (@svovaf)
16204 * @since 1.0.4
16205 *
16206 * @param bool|string $topic
16207 * @param bool|string $message
16208 * @param bool|string $summary Since 2.5.1.
16209 *
16210 * @return string
16211 */
16212 function contact_url( $topic = false, $message = false, $summary = false ) {
16213 $params = array();
16214 if ( is_string( $topic ) ) {
16215 $params['topic'] = $topic;
16216 }
16217 if ( is_string( $message ) ) {
16218 $params['message'] = $message;
16219 }
16220
16221 if ( is_string( $summary ) ) {
16222 $params['summary'] = $summary;
16223 }
16224
16225 if ( $this->is_addon() ) {
16226 $params['addon_id'] = $this->get_id();
16227
16228 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16229 } else {
16230 return $this->_get_admin_page_url( 'contact', $params );
16231 }
16232 }
16233
16234 /**
16235 * Add-on direct info URL.
16236 *
16237 * @author Vova Feldman (@svovaf)
16238 * @since 1.1.0
16239 *
16240 * @param string $slug
16241 *
16242 * @return string
16243 */
16244 function addon_url( $slug ) {
16245 return $this->_get_admin_page_url( 'addons', array(
16246 'slug' => $slug
16247 ) );
16248 }
16249
16250 /**
16251 * Add-ons URL.
16252 *
16253 * @author Vova Feldman (@svovaf)
16254 * @since 2.4.5
16255 *
16256 * @return string
16257 */
16258 function get_addons_url() {
16259 return $this->_get_admin_page_url( 'addons' );
16260 }
16261
16262 /* Logger
16263 ------------------------------------------------------------------------------------------------------------------*/
16264 /**
16265 * @param string $id
16266 * @param bool $prefix_slug
16267 *
16268 * @return FS_Logger
16269 */
16270 function get_logger( $id = '', $prefix_slug = true ) {
16271 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16272 }
16273
16274 /**
16275 * Note: This method is used externally so don't delete it.
16276 *
16277 * @param $id
16278 * @param bool $load_options
16279 * @param bool $prefix_slug
16280 *
16281 * @return FS_Option_Manager
16282 */
16283 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16284 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16285 }
16286
16287 /* Security
16288 ------------------------------------------------------------------------------------------------------------------*/
16289 private static function _encrypt( $str ) {
16290 if ( is_null( $str ) ) {
16291 return null;
16292 }
16293
16294 /**
16295 * The encrypt/decrypt functions are used to protect
16296 * the user from messing up with some of the sensitive
16297 * data stored for the module as a JSON in the database.
16298 *
16299 * I used the same suggested hack by the theme review team.
16300 * For more details, look at the function `Base64UrlDecode()`
16301 * in `./sdk/FreemiusBase.php`.
16302 *
16303 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16304 *
16305 * @author Vova Feldman (@svovaf)
16306 * @since 1.2.2
16307 */
16308 $fn = 'base64' . '_encode';
16309
16310 return $fn( $str );
16311 }
16312
16313 static function _decrypt( $str ) {
16314 if ( is_null( $str ) ) {
16315 return null;
16316 }
16317
16318 /**
16319 * The encrypt/decrypt functions are used to protect
16320 * the user from messing up with some of the sensitive
16321 * data stored for the module as a JSON in the database.
16322 *
16323 * I used the same suggested hack by the theme review team.
16324 * For more details, look at the function `Base64UrlDecode()`
16325 * in `./sdk/FreemiusBase.php`.
16326 *
16327 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16328 *
16329 * @author Vova Feldman (@svovaf)
16330 * @since 1.2.2
16331 */
16332 $fn = 'base64' . '_decode';
16333
16334 return $fn( $str );
16335 }
16336
16337 /**
16338 * @author Vova Feldman (@svovaf)
16339 * @since 1.0.5
16340 *
16341 * @param FS_Entity $entity
16342 *
16343 * @return FS_Entity Return an encrypted clone entity.
16344 */
16345 private static function _encrypt_entity( FS_Entity $entity ) {
16346 $clone = clone $entity;
16347 $props = get_object_vars( $entity );
16348
16349 foreach ( $props as $key => $val ) {
16350 $clone->{$key} = self::_encrypt( $val );
16351 }
16352
16353 return $clone;
16354 }
16355
16356 /**
16357 * @author Vova Feldman (@svovaf)
16358 * @since 1.0.5
16359 *
16360 * @param FS_Entity $entity
16361 *
16362 * @return FS_Entity Return an decrypted clone entity.
16363 */
16364 private static function decrypt_entity( FS_Entity $entity ) {
16365 $clone = clone $entity;
16366 $props = get_object_vars( $entity );
16367
16368 foreach ( $props as $key => $val ) {
16369 $clone->{$key} = self::_decrypt( $val );
16370 }
16371
16372 return $clone;
16373 }
16374
16375 /**
16376 * @author Vova Feldman (@svovaf)
16377 * @since 1.0.7
16378 *
16379 * @param string $email
16380 *
16381 * @return FS_User|false
16382 */
16383 public static function _get_user_by_email( $email ) {
16384 self::$_static_logger->entrance();
16385
16386 $email = trim( strtolower( $email ) );
16387
16388 $users = self::get_all_users();
16389
16390 if ( is_array( $users ) ) {
16391 foreach ( $users as $user ) {
16392 if ( $email === trim( strtolower( $user->email ) ) ) {
16393 return $user;
16394 }
16395 }
16396 }
16397
16398 return false;
16399 }
16400
16401 #----------------------------------------------------------------------------------
16402 #region Account (Loading, Updates & Activation)
16403 #----------------------------------------------------------------------------------
16404
16405 /***
16406 * Load account information (user + site).
16407 *
16408 * @author Vova Feldman (@svovaf)
16409 * @since 1.0.1
16410 */
16411 private function _load_account() {
16412 $this->_logger->entrance();
16413
16414 $this->do_action( 'before_account_load' );
16415
16416 $users = self::get_all_users();
16417 $plans = self::get_all_plans( $this->_module_type );
16418
16419 if ( $this->_logger->is_on() && is_admin() ) {
16420 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16421 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16422 }
16423
16424 $site = fs_is_network_admin() ?
16425 $this->get_network_install() :
16426 $this->get_install_by_blog_id();
16427
16428 if ( fs_is_network_admin() &&
16429 $this->is_network_active() &&
16430 ! is_object( $site ) &&
16431 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16432 ) {
16433 $first_install = $this->find_first_install();
16434
16435 if ( is_null( $first_install ) ) {
16436 unset( $this->_storage->network_install_blog_id );
16437 } else {
16438 $site = $first_install['install'];
16439 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16440 }
16441 }
16442
16443 if ( is_object( $site ) &&
16444 is_numeric( $site->id ) &&
16445 is_numeric( $site->user_id ) &&
16446 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16447 ) {
16448 // Load site.
16449 $this->_site = $site;
16450 }
16451
16452 $user = null;
16453 if ( fs_is_network_admin() && $this->_is_network_active ) {
16454 $user = $this->get_network_user();
16455 }
16456
16457 if ( is_object( $user ) ) {
16458 $this->_user = clone $user;
16459 } else if ( $this->_site ) {
16460 $user = self::_get_user_by_id( $this->_site->user_id );
16461
16462 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16463 /**
16464 * Try to load the previous owner. This recovery is used for the following use-case:
16465 * 1. Opt-in
16466 * 2. Cloning site1 to site2
16467 * 3. Ownership switch in site1 (same applies for site2)
16468 * 4. Install data sync on site2
16469 * 5. Now site2's install is associated with the new owner which does not exists locally.
16470 */
16471 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16472 }
16473
16474 if ( ! is_object( $user ) ) {
16475 /**
16476 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16477 */
16478 if (
16479 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16480 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16481 ) {
16482 $user = $this->sync_user_by_current_install();
16483 } else {
16484 return;
16485 }
16486
16487 if ( is_object( $user ) ) {
16488 $this->_storage->user_was_recovered_from_install = true;
16489 } else {
16490 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16491 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16492 1;
16493
16494 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16495 $this->delete_current_install( false );
16496 } else {
16497 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16498
16499 return;
16500 }
16501 }
16502 }
16503
16504 $this->_user = ( $user instanceof FS_User ) ?
16505 clone $user :
16506 null;
16507 }
16508
16509 if ( is_object( $this->_user ) ) {
16510 // Load licenses.
16511 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16512 }
16513
16514 if ( is_object( $this->_site ) ) {
16515 // Load plans.
16516 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16517 $plans[ $this->_slug ] :
16518 array();
16519
16520 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16521 $this->_sync_plans();
16522 } else {
16523 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16524 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16525 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16526 } else {
16527 unset( $this->_plans[ $i ] );
16528 }
16529 }
16530 }
16531
16532 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16533
16534 if ( $this->_site->version != $this->get_plugin_version() ) {
16535 // If stored install version is different than current installed plugin version,
16536 // then update plugin version event.
16537 $this->update_plugin_version_event();
16538 }
16539 }
16540
16541 if ( true === $this->_storage->require_license_activation &&
16542 ! fs_request_get_bool( 'require_license', true )
16543 ) {
16544 $this->_storage->require_license_activation = false;
16545 }
16546
16547 if ( $this->is_theme() ) {
16548 $this->_register_account_hooks();
16549 }
16550
16551 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16552 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16553 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16554 }
16555
16556 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16557 $this->send_pending_clone_update_once();
16558 }
16559 }
16560
16561 /**
16562 * Special user recovery mechanism.
16563 *
16564 * @author Vova Feldman (@svovaf)
16565 * @since 2.0.0
16566 *
16567 * @param number|null $site_user_id
16568 *
16569 * @return \FS_User|mixed
16570 */
16571 private function sync_user_by_current_install( $site_user_id = null ) {
16572 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16573 $site_user_id :
16574 $this->_site->user_id;
16575
16576 $api = $this->get_api_site_scope();
16577
16578 $uid = $this->get_anonymous_id();
16579 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16580
16581 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16582
16583 if ( $this->is_api_result_entity( $result ) ) {
16584 $user = new FS_User( $result );
16585 $this->_user = $user;
16586 $this->_store_user();
16587
16588 return $user;
16589 }
16590
16591 $error_code = FS_Api::get_error_code( $result );
16592
16593 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16594 /**
16595 * Those API errors will continue coming and are not recoverable with the
16596 * current site's data. Therefore, extend the API call's cached result to 7 days.
16597 */
16598 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16599 }
16600
16601 return $result;
16602 }
16603
16604 /**
16605 * @author Vova Feldman (@svovaf)
16606 * @since 1.0.1
16607 *
16608 * @param FS_User $user
16609 * @param FS_Site $site
16610 * @param bool|array $plans
16611 */
16612 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16613 $site->user_id = $user->id;
16614
16615 $this->_site = $site;
16616 $this->_user = $user;
16617 if ( false !== $plans ) {
16618 $this->_plans = $plans;
16619 }
16620
16621 $this->send_install_update();
16622
16623 $this->_store_account();
16624
16625 }
16626
16627 /**
16628 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16629 * Each version is trimmed after the 16th char.
16630 *
16631 * @author Vova Feldman (@svovaf)
16632 * @since 2.2.1
16633 *
16634 * @return array
16635 */
16636 private function get_versions() {
16637 $versions = array();
16638 $versions['sdk_version'] = $this->version;
16639
16640 // Collect these diagnostic information only if it's allowed.
16641 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16642 $versions['platform_version'] = get_bloginfo( 'version' );
16643 $versions['programming_language_version'] = phpversion();
16644 }
16645
16646 foreach ( $versions as $k => $version ) {
16647 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
16648 }
16649
16650 return $versions;
16651 }
16652
16653 /**
16654 * Get sanitized site language.
16655 *
16656 * @param string $language
16657 * @param int $max_len
16658 *
16659 * @since 2.5.1
16660 * @author Vova Feldman (@svovaf)
16661 *
16662 * @return string
16663 */
16664 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
16665 if ( empty( $language ) ) {
16666 $language = get_bloginfo( 'language' );
16667 }
16668
16669 return substr( $language, 0, $max_len );
16670 }
16671
16672 /**
16673 * Get core version stripped from pre-release and build.
16674 *
16675 * @since 2.5.1
16676 * @author Vova Feldman (@svovaf)
16677 *
16678 * @param string $version
16679 * @param int $parts
16680 * @param int $max_len
16681 * @param bool $include_pre_release
16682 *
16683 * @return string
16684 */
16685 private static function get_core_version(
16686 $version,
16687 $parts = 3,
16688 $max_len = self::VERSION_MAX_CHARS,
16689 $include_pre_release = false
16690 ) {
16691 if ( empty( $version ) ) {
16692 // Version is empty.
16693 return '';
16694 }
16695
16696 if ( is_numeric( $version ) ) {
16697 $is_float_version = is_float( $version );
16698
16699 $version = (string) $version;
16700
16701 /**
16702 * Casting a whole float number to a string cuts the decimal point. This part make sure to add the missing decimal part to the version.
16703 */
16704 if ( $is_float_version && false === strpos( $version, '.' ) ) {
16705 $version .= '.0';
16706 }
16707 }
16708
16709 if ( ! is_string( $version ) ) {
16710 return '';
16711 }
16712
16713 if ( $parts < 1 ) {
16714 return '';
16715 }
16716
16717 $pre_release_regex = $include_pre_release ?
16718 '(\-(alpha|beta|RC)([0-9]+)?)?' :
16719 '';
16720
16721 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
16722 // Version is not starting with a digit.
16723 return '';
16724 }
16725
16726 return substr( $matches[1], 0, $max_len );
16727 }
16728
16729 /**
16730 * @param string $prop
16731 * @param mixed $val
16732 *
16733 * @return mixed
16734 *@author Vova Feldman (@svovaf)
16735 *
16736 * @since 2.5.1
16737 */
16738 private static function get_api_sanitized_property( $prop, $val ) {
16739 if ( ! is_string( $val ) || empty( $val ) ) {
16740 return $val;
16741 }
16742
16743 switch ( $prop ) {
16744 case 'programming_language_version':
16745 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
16746 return self::get_core_version( $val );
16747 case 'platform_version':
16748 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
16749 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
16750 case 'sdk_version':
16751 // Get the exact SDK version, which can have up to 4 parts.
16752 return self::get_core_version( $val, 4 );
16753 case 'version':
16754 // Get the entire version but just limited in length.
16755 return substr( $val, 0, self::VERSION_MAX_CHARS );
16756 case 'language':
16757 return self::get_sanitized_language( $val );
16758 default:
16759 return $val;
16760 }
16761 }
16762
16763 /**
16764 * @author Leo Fajardo (@leorw)
16765 * @since 2.3.0
16766 *
16767 * @return bool
16768 */
16769 function has_beta_update() {
16770 return (
16771 ! empty( $this->_storage->beta_data ) &&
16772 ( true === $this->_storage->beta_data['is_beta'] ) &&
16773 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16774 );
16775 }
16776
16777 /**
16778 * @author Leo Fajardo (@leorw)
16779 * @since 2.3.0
16780 *
16781 * @return bool
16782 */
16783 function is_beta() {
16784 return (
16785 ! empty( $this->_storage->beta_data ) &&
16786 ( true === $this->_storage->beta_data['is_beta'] ) &&
16787 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16788 );
16789 }
16790
16791 /**
16792 * @author Vova Feldman (@svovaf)
16793 * @since 1.1.7.4
16794 *
16795 * @param array $override_with
16796 * @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.
16797 *
16798 * @return array
16799 */
16800 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
16801 $this->_logger->entrance();
16802
16803 $current_user = self::_get_current_wp_user();
16804
16805 $activation_action = $this->get_unique_affix() . '_activate_new';
16806 $return_url = $this->is_anonymous() ?
16807 // If skipped already, then return to the account page.
16808 $this->get_account_url( $activation_action, array(), false ) :
16809 // Return to the module's main page.
16810 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16811
16812 $versions = $this->get_versions();
16813
16814 $params = array_merge( $versions, array(
16815 'user_firstname' => $current_user->user_firstname,
16816 'user_lastname' => $current_user->user_lastname,
16817 'user_email' => $current_user->user_email,
16818 'plugin_slug' => $this->_slug,
16819 'plugin_id' => $this->get_id(),
16820 'plugin_public_key' => $this->get_public_key(),
16821 'plugin_version' => $this->get_plugin_version(),
16822 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16823 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16824 'account',
16825 array( 'fs_action' => 'sync_user' )
16826 ), 'sync_user' ),
16827 'is_premium' => $this->is_premium(),
16828 'is_active' => true,
16829 'is_uninstalled' => false,
16830 'is_localhost' => WP_FS__IS_LOCALHOST,
16831 ) );
16832
16833 if ( $this->is_addon() ) {
16834 $parent_fs = $this->get_parent_instance();
16835
16836 $params['parent_plugin_slug'] = $parent_fs->_slug;
16837 $params['parent_plugin_id'] = $parent_fs->get_id();
16838 }
16839
16840 if ( true === $network_level_or_blog_id ) {
16841 if ( ! isset( $override_with['sites'] ) ) {
16842 $params['sites'] = $this->get_sites_for_network_level_optin();
16843 }
16844 } else {
16845 $site = is_numeric( $network_level_or_blog_id ) ?
16846 array( 'blog_id' => $network_level_or_blog_id ) :
16847 null;
16848
16849 $site = $this->get_site_info( $site );
16850
16851 $diagnostic_info = array();
16852 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16853 $diagnostic_info = array(
16854 'site_name' => $site['title'],
16855 'language' => self::get_sanitized_language( $site['language'] ),
16856 );
16857 }
16858
16859 $params = array_merge( $params, $diagnostic_info, array(
16860 'site_uid' => $site['uid'],
16861 'site_url' => $site['url'],
16862 ) );
16863 }
16864
16865 if ( $this->is_pending_activation() &&
16866 ! empty( $this->_storage->pending_license_key )
16867 ) {
16868 $params['license_key'] = $this->_storage->pending_license_key;
16869 }
16870
16871 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
16872 // Even though rand() is known for its security issues,
16873 // the timestamp adds another layer of protection.
16874 // It would be very hard for an attacker to get the secret key form here.
16875 // Plus, this should never run in production since the secret should never
16876 // be included in the production version.
16877 $params['ts'] = WP_FS__SCRIPT_START_TIME;
16878 $params['salt'] = md5( uniqid( rand() ) );
16879 $params['secure'] = md5(
16880 $params['ts'] .
16881 $params['salt'] .
16882 $this->get_secret_key()
16883 );
16884 }
16885
16886 if ( is_multisite() && function_exists( 'get_network' ) ) {
16887 $params['network_uid'] = $this->get_anonymous_network_id();
16888 }
16889
16890 return array_merge( $params, $override_with );
16891 }
16892
16893 /**
16894 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
16895 * 2. If there was an API error, return the API result.
16896 *
16897 * @author Vova Feldman (@svovaf)
16898 * @since 1.1.7.4
16899 *
16900 * @param string|bool $email
16901 * @param string|bool $first
16902 * @param string|bool $last
16903 * @param string|bool $license_key
16904 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
16905 * In this case, the user and site info will be sent to the server but no
16906 * data will be saved to the WP installation's database.
16907 * @param number|bool $trial_plan_id
16908 * @param bool $is_disconnected Whether to opt in without tracking.
16909 * @param null|bool $is_marketing_allowed
16910 * @param array $sites If network-level opt-in, an array of containing details of sites.
16911 * @param bool $redirect
16912 *
16913 * @return string|object
16914 * @use WP_Error
16915 */
16916 function opt_in(
16917 $email = false,
16918 $first = false,
16919 $last = false,
16920 $license_key = false,
16921 $is_uninstall = false,
16922 $trial_plan_id = false,
16923 $is_disconnected = false,
16924 $is_marketing_allowed = null,
16925 $sites = array(),
16926 $redirect = true
16927 ) {
16928 $this->_logger->entrance();
16929
16930 if ( false === $email ) {
16931 $current_user = self::_get_current_wp_user();
16932 $email = $current_user->user_email;
16933 }
16934
16935 /**
16936 * @since 1.2.1 If activating with license key, ignore the context-user
16937 * since the user will be automatically loaded from the license.
16938 */
16939 if ( empty( $license_key ) ) {
16940 // Clean up pending license if opt-ing in again.
16941 $this->_storage->remove( 'pending_license_key' );
16942
16943 if ( ! $is_uninstall ) {
16944 $fs_user = Freemius::_get_user_by_email( $email );
16945 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
16946 return $this->install_with_user(
16947 $fs_user,
16948 false,
16949 $trial_plan_id,
16950 $redirect,
16951 true,
16952 $sites
16953 );
16954 }
16955 }
16956 }
16957
16958 $user_info = array();
16959 if ( ! empty( $email ) ) {
16960 $user_info['user_email'] = $email;
16961 }
16962 if ( ! empty( $first ) ) {
16963 $user_info['user_firstname'] = $first;
16964 }
16965 if ( ! empty( $last ) ) {
16966 $user_info['user_lastname'] = $last;
16967 }
16968
16969 if ( ! empty( $sites ) ) {
16970 $is_network = true;
16971
16972 $user_info['sites'] = $sites;
16973 } else {
16974 $is_network = false;
16975 }
16976
16977 $params = $this->get_opt_in_params( $user_info, $is_network );
16978
16979 $filtered_license_key = false;
16980 if ( is_string( $license_key ) ) {
16981 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
16982 $params['license_key'] = $filtered_license_key;
16983 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
16984 $params['trial_plan_id'] = $trial_plan_id;
16985 }
16986
16987 if ( $is_uninstall ) {
16988 $params['uninstall_params'] = array(
16989 'reason_id' => $this->_storage->uninstall_reason->id,
16990 'reason_info' => $this->_storage->uninstall_reason->info
16991 );
16992 }
16993
16994 if ( isset( $params['license_key'] ) ) {
16995 $fs_user = Freemius::_get_user_by_email( $email );
16996
16997 if ( is_object( $fs_user ) ) {
16998 /**
16999 * If opting in with a context license and the context WP Admin user already opted in
17000 * before from the current site, add the user context security params to avoid the
17001 * unnecessary email activation when the context license is owned by the same context user.
17002 *
17003 * @author Leo Fajardo (@leorw)
17004 * @since 1.2.3
17005 */
17006 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17007 $fs_user,
17008 false,
17009 'install_with_existing_user'
17010 ) );
17011 }
17012 }
17013
17014 if ( is_bool( $is_marketing_allowed ) ) {
17015 $params['is_marketing_allowed'] = $is_marketing_allowed;
17016 }
17017
17018 $params['is_disconnected'] = $is_disconnected;
17019 $params['format'] = 'json';
17020 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17021 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17022
17023 $request = array(
17024 'method' => 'POST',
17025 'body' => $params,
17026 'timeout' => 60,
17027 );
17028
17029 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17030 $response = self::safe_remote_post( $url, $request );
17031
17032 if ( is_wp_error( $response ) ) {
17033 /**
17034 * @var WP_Error $response
17035 */
17036 $result = new stdClass();
17037
17038 $error_code = $response->get_error_code();
17039 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17040
17041 $result->error = (object) array(
17042 'type' => $error_type,
17043 'message' => $response->get_error_message(),
17044 'code' => $error_code,
17045 'http' => 402
17046 );
17047
17048 $this->maybe_modify_api_curl_error_message( $result );
17049
17050 if ( FS_Api::is_blocked( $result ) ) {
17051 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17052 }
17053
17054 $is_connected = null;
17055
17056 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17057 $this->skip_connection( fs_is_network_admin() );
17058
17059 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17060 }
17061
17062 $this->update_connectivity_info( $is_connected );
17063
17064 return $result;
17065 }
17066
17067 $this->update_connectivity_info( true );
17068
17069 // Module is being uninstalled, don't handle the returned data.
17070 if ( $is_uninstall ) {
17071 return true;
17072 }
17073
17074 /**
17075 * 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.
17076 *
17077 * @author Vova Feldman (@svovaf)
17078 * @since 1.2.3
17079 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17080 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17081 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17082 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17083 */
17084 $decoded = is_string( $response['body'] ) ?
17085 json_decode( $response['body'] ) :
17086 null;
17087
17088 if ( empty( $decoded ) ) {
17089 return false;
17090 }
17091
17092 if ( ! $this->is_api_result_object( $decoded ) ) {
17093 if ( ! empty( $params['license_key'] ) ) {
17094 // Pass the fully entered license key to the failure handler.
17095 $params['license_key'] = $license_key;
17096 }
17097
17098 return $is_uninstall ?
17099 $decoded :
17100 $this->apply_filters( 'after_install_failure', $decoded, $params );
17101 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17102 if ( $is_network ) {
17103 $site_ids = array();
17104 foreach ( $sites as $site ) {
17105 $site_ids[] = $site['blog_id'];
17106 }
17107
17108 /**
17109 * Store the sites so that they can be installed once the user has clicked on the activation link
17110 * in the email.
17111 *
17112 * @author Leo Fajardo (@leorw)
17113 */
17114 $this->_storage->pending_sites_info = array(
17115 'blog_ids' => $site_ids,
17116 'license_key' => $license_key,
17117 'trial_plan_id' => $trial_plan_id
17118 );
17119 }
17120
17121 // Pending activation, add message.
17122 return $this->set_pending_confirmation(
17123 ( isset( $decoded->email ) ?
17124 $decoded->email :
17125 true ),
17126 false,
17127 $filtered_license_key,
17128 ! empty( $params['trial_plan_id'] ),
17129 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17130 );
17131 } else if ( isset( $decoded->install_secret_key ) ) {
17132 return $this->install_with_new_user(
17133 $decoded->user_id,
17134 $decoded->user_public_key,
17135 $decoded->user_secret_key,
17136 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17137 $decoded->is_marketing_allowed :
17138 null ),
17139 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17140 $decoded->is_extensions_tracking_allowed :
17141 null ),
17142 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17143 $decoded->is_diagnostic_tracking_allowed :
17144 null ),
17145 $decoded->install_id,
17146 $decoded->install_public_key,
17147 $decoded->install_secret_key,
17148 false
17149 );
17150 } else if ( is_array( $decoded->installs ) ) {
17151 return $this->install_many_with_new_user(
17152 $decoded->user_id,
17153 $decoded->user_public_key,
17154 $decoded->user_secret_key,
17155 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17156 $decoded->is_marketing_allowed :
17157 null ),
17158 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17159 $decoded->is_extensions_tracking_allowed :
17160 null ),
17161 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17162 $decoded->is_diagnostic_tracking_allowed :
17163 null ),
17164 $decoded->installs,
17165 false
17166 );
17167 }
17168
17169 return $decoded;
17170 }
17171
17172 /**
17173 * Set user and site identities.
17174 *
17175 * @author Vova Feldman (@svovaf)
17176 * @since 1.0.9
17177 *
17178 * @param FS_User $user
17179 * @param FS_Site $site
17180 * @param bool $redirect
17181 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17182 * redirect (or return a URL) to the account page with a special parameter to
17183 * trigger the auto installation processes.
17184 *
17185 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17186 */
17187 function setup_account(
17188 FS_User $user,
17189 FS_Site $site,
17190 $redirect = true,
17191 $auto_install = false
17192 ) {
17193 return $this->setup_network_account(
17194 $user,
17195 array( $site ),
17196 $redirect,
17197 $auto_install,
17198 false
17199 );
17200 }
17201
17202 /**
17203 * Set user and site identities.
17204 *
17205 * @author Vova Feldman (@svovaf)
17206 * @since 2.0.0
17207 *
17208 * @param FS_User $user
17209 * @param FS_Site[] $installs
17210 * @param bool $redirect
17211 * @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.
17212 * @param bool $is_network_level_opt_in
17213 *
17214 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17215 */
17216 function setup_network_account(
17217 FS_User $user,
17218 array $installs,
17219 $redirect = true,
17220 $auto_install = false,
17221 $is_network_level_opt_in = true
17222 ) {
17223 $first_install = $installs[0];
17224
17225 $this->_user = $user;
17226 $this->_site = $first_install;
17227
17228 $this->_sync_plans();
17229
17230 if ( $this->_storage->handle_gdpr_admin_notice &&
17231 $this->should_handle_gdpr_admin_notice() &&
17232 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17233 ) {
17234 /**
17235 * Clear user lock after an opt-in.
17236 */
17237 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17238 FS_User_Lock::instance()->unlock();
17239 }
17240
17241 if ( 1 < count( $installs ) ) {
17242 // Only network level opt-in can have more than one install.
17243 $is_network_level_opt_in = true;
17244 }
17245
17246 $this->update_connectivity_info( true );
17247
17248 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17249 // If Freemius was OFF before, turn it on.
17250 $this->turn_on();
17251
17252 $this->handle_account_connection(
17253 $installs,
17254 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17255 );
17256
17257 if ( is_numeric( $first_install->license_id ) ) {
17258 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17259 }
17260
17261 $this->_admin_notices->remove_sticky( 'connect_account' );
17262
17263 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17264 $this->clear_pending_activation_mode();
17265
17266 if ( ! $this->is_paying_or_trial() ) {
17267 $this->_admin_notices->add_sticky(
17268 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17269 'activation_complete'
17270 );
17271 }
17272 }
17273
17274 if ( $this->is_paying_or_trial() ) {
17275 if ( ! $this->is_premium() ||
17276 ! $this->has_premium_version() ||
17277 ! $this->has_settings_menu()
17278 ) {
17279 if ( $this->is_paying() ) {
17280 $this->add_complete_upgrade_instructions_notice(
17281 sprintf(
17282 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17283 $this->get_plan_title()
17284 ),
17285 'plan_upgraded'
17286 );
17287 } else {
17288 $trial_plan = $this->get_trial_plan();
17289
17290 $this->add_complete_upgrade_instructions_notice(
17291 sprintf(
17292 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17293 '<i>' . $this->get_plugin_name() . '</i>'
17294 ),
17295 'trial_started',
17296 $trial_plan->title
17297 );
17298 }
17299 }
17300
17301 $this->_admin_notices->remove_sticky( array(
17302 'trial_promotion',
17303 ) );
17304 }
17305
17306 $plugin_id = fs_request_get( 'plugin_id', false );
17307
17308 // Store activation time ONLY for plugins & themes (not add-ons).
17309 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17310 if ( empty( $this->_storage->activation_timestamp ) ) {
17311 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17312 }
17313 }
17314
17315 $next_page = '';
17316
17317 $extra = array();
17318 if ( $auto_install ) {
17319 $extra['auto_install'] = 'true';
17320 }
17321
17322 if ( is_numeric( $plugin_id ) ) {
17323 /**
17324 * @author Leo Fajardo (@leorw)
17325 * @since 1.2.1.6
17326 *
17327 * Also sync the license after an anonymous user subscribes.
17328 */
17329 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17330 // Add-on was installed - sync license right after install.
17331 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17332 }
17333 } else {
17334 /**
17335 * @author Vova Feldman (@svovaf)
17336 * @since 1.1.9 If site installed with a valid license, sync license.
17337 */
17338 if ( $this->is_paying() ) {
17339 $this->_sync_plugin_license(
17340 true,
17341 // Installs data is already synced in the beginning of this method directly or via _set_account().
17342 false
17343 );
17344 }
17345
17346 // Reload the page with the keys.
17347 $next_page = $this->is_anonymous() ?
17348 // If user previously skipped, redirect to account page.
17349 $this->get_account_url( false, $extra ) :
17350 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17351 }
17352
17353 if ( ! empty( $next_page ) && $redirect ) {
17354 fs_redirect( $next_page );
17355 }
17356
17357 return $next_page;
17358 }
17359
17360 /**
17361 * Install plugin with new user information after approval.
17362 *
17363 * @author Vova Feldman (@svovaf)
17364 * @since 1.0.7
17365 */
17366 function _install_with_new_user() {
17367 $this->_logger->entrance();
17368
17369 if ( $this->is_registered() ) {
17370 return;
17371 }
17372
17373 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17374
17375 $this->update_license_required_permissions_if_anonymous();
17376
17377 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17378 // @todo This logic should be improved because it's executed on every load of a theme.
17379 $this->is_theme()
17380 ) {
17381 // check_admin_referer( $this->_slug . '_activate_new' );
17382
17383 if ( fs_request_has( 'user_secret_key' ) ) {
17384 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17385 $pending_sites_info = $this->_storage->pending_sites_info;
17386
17387 $this->install_many_pending_with_user(
17388 fs_request_get( 'user_id' ),
17389 fs_request_get_raw( 'user_public_key' ),
17390 fs_request_get_raw( 'user_secret_key' ),
17391 fs_request_get_bool( 'is_marketing_allowed', null ),
17392 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17393 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17394 $pending_sites_info['blog_ids'],
17395 $pending_sites_info['license_key'],
17396 $pending_sites_info['trial_plan_id']
17397 );
17398 } else {
17399 $this->install_with_new_user(
17400 fs_request_get( 'user_id' ),
17401 fs_request_get_raw( 'user_public_key' ),
17402 fs_request_get_raw( 'user_secret_key' ),
17403 fs_request_get_bool( 'is_marketing_allowed', null ),
17404 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17405 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17406 fs_request_get( 'install_id' ),
17407 fs_request_get_raw( 'install_public_key' ),
17408 fs_request_get_raw( 'install_secret_key' ),
17409 true,
17410 fs_request_get_bool( 'auto_install' )
17411 );
17412 }
17413 } else if ( $has_pending_activation_confirmation_param ) {
17414 $this->set_pending_confirmation(
17415 fs_request_get( 'user_email' ),
17416 true,
17417 false,
17418 false,
17419 fs_request_get_bool( 'is_suspicious_email' ),
17420 fs_request_get_bool( 'has_upgrade_context' ),
17421 fs_request_get( 'support_email_address' )
17422 );
17423 }
17424 }
17425 }
17426
17427 /**
17428 * @author Vova Feldman (@svovaf)
17429 * @since 2.0.0
17430 *
17431 * @param number $id
17432 * @param string $public_key
17433 * @param string $secret_key
17434 *
17435 * @return \FS_User
17436 */
17437 private function setup_user( $id, $public_key, $secret_key ) {
17438 $user = self::_get_user_by_id( $id );
17439
17440 if ( is_object( $user ) ) {
17441 $this->_user = $user;
17442 } else {
17443 $user = new FS_User();
17444 $user->id = $id;
17445 $user->public_key = $public_key;
17446 $user->secret_key = $secret_key;
17447
17448 $this->_user = $user;
17449 $user_result = $this->get_api_user_scope()->get();
17450 $user = new FS_User( $user_result );
17451
17452 $this->_user = $user;
17453 $this->_store_user();
17454 }
17455
17456 return $user;
17457 }
17458
17459 /**
17460 * Install plugin with new user.
17461 *
17462 * @author Vova Feldman (@svovaf)
17463 * @since 1.1.7.4
17464 *
17465 * @param number $user_id
17466 * @param string $user_public_key
17467 * @param string $user_secret_key
17468 * @param bool|null $is_marketing_allowed
17469 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17470 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17471 * @param number $install_id
17472 * @param string $install_public_key
17473 * @param string $install_secret_key
17474 * @param bool $redirect
17475 * @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.
17476 *
17477 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17478 */
17479 private function install_with_new_user(
17480 $user_id,
17481 $user_public_key,
17482 $user_secret_key,
17483 $is_marketing_allowed,
17484 $is_extensions_tracking_allowed,
17485 $is_diagnostic_tracking_allowed,
17486 $install_id,
17487 $install_public_key,
17488 $install_secret_key,
17489 $redirect = true,
17490 $auto_install = false
17491 ) {
17492 /**
17493 * This method is also executed after opting in with a license key since the
17494 * license can be potentially associated with a different owner.
17495 *
17496 * @since 2.0.0
17497 */
17498 $user = self::_get_user_by_id( $user_id );
17499
17500 if ( ! is_object( $user ) ) {
17501 $user = new FS_User();
17502 $user->id = $user_id;
17503 $user->public_key = $user_public_key;
17504 $user->secret_key = $user_secret_key;
17505
17506 $this->_user = $user;
17507 $user_result = $this->get_api_user_scope()->get();
17508 $user = new FS_User( $user_result );
17509 }
17510
17511 $this->_user = $user;
17512
17513 $site = new FS_Site();
17514 $site->id = $install_id;
17515 $site->public_key = $install_public_key;
17516 $site->secret_key = $install_secret_key;
17517
17518 $this->_site = $site;
17519 $site_result = $this->get_api_site_scope( true )->get();
17520 $site = new FS_Site( $site_result );
17521 $this->_site = $site;
17522
17523 if ( ! is_null( $is_marketing_allowed ) ) {
17524 $this->disable_opt_in_notice_and_lock_user();
17525 }
17526
17527 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17528 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17529 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17530 ) );
17531
17532 return $this->setup_account(
17533 $this->_user,
17534 $this->_site,
17535 $redirect,
17536 $auto_install
17537 );
17538 }
17539
17540 /**
17541 * Install plugin with user.
17542 *
17543 * @author Leo Fajardo (@leorw)
17544 * @since 2.0.0
17545 *
17546 * @param number $user_id
17547 * @param string $user_public_key
17548 * @param string $user_secret_key
17549 * @param bool|null $is_marketing_allowed
17550 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17551 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17552 * @param array $site_ids
17553 * @param bool $license_key
17554 * @param bool $trial_plan_id
17555 * @param bool $redirect
17556 *
17557 * @return void
17558 */
17559 private function install_many_pending_with_user(
17560 $user_id,
17561 $user_public_key,
17562 $user_secret_key,
17563 $is_marketing_allowed,
17564 $is_extensions_tracking_allowed,
17565 $is_diagnostic_tracking_allowed,
17566 $site_ids,
17567 $license_key = false,
17568 $trial_plan_id = false,
17569 $redirect = true
17570 ) {
17571 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17572
17573 if ( ! is_null( $is_marketing_allowed ) ) {
17574 $this->disable_opt_in_notice_and_lock_user();
17575 }
17576
17577 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17578 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17579 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17580 ) );
17581
17582 $sites = array();
17583 foreach ( $site_ids as $site_id ) {
17584 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17585 }
17586
17587 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17588 }
17589
17590 /**
17591 * Multi-site install with a new user.
17592 *
17593 * @author Vova Feldman (@svovaf)
17594 * @since 2.0.0
17595 *
17596 * @param number $user_id
17597 * @param string $user_public_key
17598 * @param string $user_secret_key
17599 * @param bool|null $is_marketing_allowed
17600 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17601 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17602 * @param object[] $installs
17603 * @param bool $redirect
17604 * @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.
17605 *
17606 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17607 */
17608 private function install_many_with_new_user(
17609 $user_id,
17610 $user_public_key,
17611 $user_secret_key,
17612 $is_marketing_allowed,
17613 $is_extensions_tracking_allowed,
17614 $is_diagnostic_tracking_allowed,
17615 array $installs,
17616 $redirect = true,
17617 $auto_install = false
17618 ) {
17619 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17620
17621 if ( ! is_null( $is_marketing_allowed ) ) {
17622 $this->disable_opt_in_notice_and_lock_user();
17623 }
17624
17625 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17626 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17627 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17628 ) );
17629
17630 $install_ids = array();
17631
17632 foreach ( $installs as $install ) {
17633 $install_ids[] = $install->id;
17634 }
17635
17636 $items_per_request = 25;
17637 $left = count( $install_ids );
17638 $offset = 0;
17639
17640 $installs = array();
17641 while ( $left > 0 ) {
17642 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
17643
17644 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17645 // @todo Handle API error.
17646 }
17647
17648 $installs = array_merge( $installs, $result->installs );
17649
17650 $left -= $items_per_request;
17651 $offset += $items_per_request;
17652 }
17653
17654 foreach ( $installs as &$install ) {
17655 $install = new FS_Site( $install );
17656 }
17657
17658 return $this->setup_network_account(
17659 $this->_user,
17660 $installs,
17661 $redirect,
17662 $auto_install
17663 );
17664 }
17665
17666 /**
17667 * @author Vova Feldman (@svovaf)
17668 * @since 1.1.7.4
17669 *
17670 * @param string|bool $email
17671 * @param bool $redirect
17672 * @param string|bool $license_key Since 1.2.1.5
17673 * @param bool $is_pending_trial Since 1.2.1.5
17674 * @param bool $is_suspicious_email Since 2.5.0
17675 * @param bool $has_upgrade_context Since 2.5.3
17676 * @param bool|string $support_email_address Since 2.5.3
17677 *
17678 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17679 */
17680 private function set_pending_confirmation(
17681 $email = false,
17682 $redirect = true,
17683 $license_key = false,
17684 $is_pending_trial = false,
17685 $is_suspicious_email = false,
17686 $has_upgrade_context = false,
17687 $support_email_address = false
17688 ) {
17689 $is_network_admin = fs_is_network_admin();
17690
17691 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
17692 /**
17693 * If explicitly asked to ignore pending mode, set to anonymous mode
17694 * if require confirmation before finalizing the opt-in except after completing a purchase (otherwise, in this case, they wouldn't see any notice telling them that they should receive their license key via email).
17695 *
17696 * @author Vova Feldman
17697 * @since 1.2.1.6
17698 */
17699 $this->skip_connection( $is_network_admin );
17700 } else {
17701 // Install must be activated via email since
17702 // user with the same email already exist.
17703 $this->_storage->is_pending_activation = true;
17704 $this->_add_pending_activation_notice(
17705 $email,
17706 $is_pending_trial,
17707 $is_suspicious_email,
17708 $has_upgrade_context,
17709 $support_email_address
17710 );
17711 }
17712
17713 if ( ! empty( $license_key ) ) {
17714 $this->_storage->pending_license_key = $license_key;
17715 }
17716
17717 // Remove the opt-in sticky notice.
17718 $this->_admin_notices->remove_sticky( array(
17719 'connect_account',
17720 'trial_promotion',
17721 ) );
17722
17723 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17724
17725 if ( $redirect ) {
17726 // Reload the page with a pending activation message.
17727 fs_redirect( $next_page );
17728 }
17729
17730 return $next_page;
17731 }
17732
17733 /**
17734 * Install plugin with current logged WP user info.
17735 *
17736 * @author Vova Feldman (@svovaf)
17737 * @since 1.0.7
17738 */
17739 function _install_with_current_user() {
17740 $this->_logger->entrance();
17741
17742 if ( $this->is_registered() ) {
17743 return;
17744 }
17745
17746 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17747 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
17748
17749 /**
17750 * @author Vova Feldman (@svovaf)
17751 * @since 1.1.9 Add license key if given.
17752 */
17753 $license_key = fs_request_get_raw( 'license_secret_key' );
17754
17755 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17756 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17757 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17758 ) );
17759
17760 $this->install_with_current_user( $license_key );
17761 }
17762 }
17763
17764
17765 /**
17766 * @author Vova Feldman (@svovaf)
17767 * @since 1.1.7.4
17768 *
17769 * @param string|bool $license_key
17770 * @param number|bool $trial_plan_id
17771 * @param array $sites Since 2.0.0
17772 * @param bool $redirect
17773 *
17774 * @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.
17775 */
17776 function install_with_current_user(
17777 $license_key = false,
17778 $trial_plan_id = false,
17779 $sites = array(),
17780 $redirect = true
17781 ) {
17782 // Get current logged WP user.
17783 $current_user = self::_get_current_wp_user();
17784
17785 // Find the relevant FS user by the email.
17786 $user = self::_get_user_by_email( $current_user->user_email );
17787
17788 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17789 }
17790
17791 /**
17792 * @author Vova Feldman (@svovaf)
17793 * @since 2.0.0
17794 *
17795 * @param \FS_User $user
17796 * @param string|bool $license_key
17797 * @param number|bool $trial_plan_id
17798 * @param bool $redirect
17799 * @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.
17800 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
17801 *
17802 * @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.
17803 */
17804 function install_with_user(
17805 FS_User $user,
17806 $license_key = false,
17807 $trial_plan_id = false,
17808 $redirect = true,
17809 $setup_account = true,
17810 $sites = array()
17811 ) {
17812 // We have to set the user before getting user scope API handler.
17813 $this->_user = $user;
17814
17815 // Install the plugin.
17816 $result = $this->create_installs_with_user(
17817 $user,
17818 $license_key,
17819 $trial_plan_id,
17820 $sites,
17821 $redirect
17822 );
17823
17824 if ( ! $this->is_api_result_entity( $result ) &&
17825 ! $this->is_api_result_object( $result, 'installs' )
17826 ) {
17827 // @todo Handler potential API error of the $result
17828 }
17829
17830 if ( empty( $sites ) ) {
17831 $site = new FS_Site( $result );
17832 $this->_site = $site;
17833
17834 if ( ! $setup_account ) {
17835 $this->_store_site();
17836
17837 $this->sync_plan_if_not_exist( $site->plan_id );
17838
17839 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
17840 $this->sync_license_if_not_exist( $site->license_id, $license_key );
17841 }
17842
17843 $this->_admin_notices->remove_sticky( 'connect_account', false );
17844
17845 return $site;
17846 }
17847
17848 return $this->setup_account( $this->_user, $this->_site, $redirect );
17849 } else {
17850 $installs = array();
17851 foreach ( $result->installs as $install ) {
17852 $installs[] = new FS_Site( $install );
17853 }
17854
17855 return $this->setup_network_account(
17856 $user,
17857 $installs,
17858 $redirect
17859 );
17860 }
17861 }
17862
17863 /**
17864 * Initiate an API request to create a collection of installs.
17865 *
17866 * @author Vova Feldman (@svovaf)
17867 * @since 2.0.0
17868 *
17869 * @param \FS_User $user
17870 * @param bool $license_key
17871 * @param bool $trial_plan_id
17872 * @param array $sites
17873 * @param bool $redirect
17874 * @param bool $silent
17875 *
17876 * @return object|mixed
17877 */
17878 private function create_installs_with_user(
17879 FS_User $user,
17880 $license_key = false,
17881 $trial_plan_id = false,
17882 $sites = array(),
17883 $redirect = false,
17884 $silent = false
17885 ) {
17886 $extra_install_params = array(
17887 'uid' => $this->get_anonymous_id(),
17888 'is_disconnected' => false,
17889 );
17890
17891 if ( ! empty( $license_key ) ) {
17892 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
17893
17894 if ( $silent ) {
17895 $extra_install_params['ignore_license_owner'] = true;
17896 }
17897 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17898 $extra_install_params['trial_plan_id'] = $trial_plan_id;
17899 }
17900
17901 if ( ! empty( $sites ) ) {
17902 $extra_install_params['sites'] = $sites;
17903 }
17904
17905 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
17906
17907 // Install the plugin.
17908 $result = $this->get_api_user_scope_by_user( $user )->call(
17909 "/plugins/{$this->get_id()}/installs.json",
17910 'post',
17911 $args
17912 );
17913
17914 if ( ! $this->is_api_result_entity( $result ) &&
17915 ! $this->is_api_result_object( $result, 'installs' )
17916 ) {
17917 if ( ! empty( $args['license_key'] ) ) {
17918 // Pass the fully entered license key to the failure handler.
17919 $args['license_key'] = $license_key;
17920 }
17921
17922 $result = $this->apply_filters( 'after_install_failure', $result, $args );
17923
17924 if ( ! $silent ) {
17925 $this->_admin_notices->add(
17926 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17927 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
17928 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17929 'error'
17930 );
17931 }
17932
17933 if ( $redirect ) {
17934 /**
17935 * We set the user before getting the user scope API handler, so the user became temporarily
17936 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
17937 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
17938 * activation page based on the return value of `is_registered()`. In addition, in case the
17939 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
17940 * misleading plugin activation errors will be shown on the `Plugins` page.
17941 *
17942 * @author Leo Fajardo (@leorw)
17943 */
17944 $user = $this->_user;
17945
17946 $this->_user = null;
17947
17948 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
17949
17950 /**
17951 * Restore the user after the redirect, this is relevant when there are cases where the redirect will choose not to do anything.
17952 */
17953 $this->_user = $user;
17954 }
17955 }
17956
17957 return $result;
17958 }
17959
17960 /**
17961 * Tries to activate add-on account based on parent plugin info.
17962 *
17963 * @author Vova Feldman (@svovaf)
17964 * @since 1.0.6
17965 *
17966 * @param Freemius $parent_fs
17967 * @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.
17968 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
17969 */
17970 private function _activate_addon_account(
17971 Freemius $parent_fs,
17972 $network_level_or_blog_id = null,
17973 FS_Plugin_License $bundle_license = null
17974 ) {
17975 if ( $this->is_registered() ) {
17976 // Already activated.
17977 return;
17978 }
17979
17980 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
17981 $permissions = array();
17982 foreach ( $permission_ids as $permission_id ) {
17983 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
17984 }
17985
17986 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
17987
17988 /**
17989 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
17990 * already returns the data for the current blog.
17991 *
17992 * @author Leo Fajardo (@leorw)
17993 * @since 2.3.0
17994 */
17995 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
17996 array() :
17997 array( 'uid' => $this->get_anonymous_id() );
17998
17999 $params = $this->get_install_data_for_api(
18000 $uid_param_to_override,
18001 false,
18002 false,
18003 /**
18004 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18005 * already includes the data for it.
18006 *
18007 * @author Leo Fajardo (@leorw)
18008 * @since 2.3.0
18009 */
18010 ( true !== $network_level_or_blog_id )
18011 );
18012
18013 if ( true === $network_level_or_blog_id ) {
18014 $params['sites'] = $this->get_sites_for_network_level_optin();
18015
18016 if ( empty( $params['sites'] ) ) {
18017 return;
18018 }
18019 }
18020
18021 if ( is_object( $bundle_license ) ) {
18022 $params['license_key'] = $bundle_license->secret_key;
18023 }
18024
18025 // Activate add-on with parent plugin credentials.
18026 $result = $parent_fs->get_api_site_scope()->call(
18027 "/addons/{$this->_plugin->id}/installs.json",
18028 'post',
18029 $params
18030 );
18031
18032 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18033 if ( is_object( $bundle_license ) ) {
18034 /**
18035 * 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.
18036 *
18037 * @author Leo Fajardo (@leorw)
18038 * @since 2.4.0
18039 */
18040 } else {
18041 $error_message = FS_Api::is_api_error_object( $result ) ?
18042 $result->error->message :
18043 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18044
18045 $this->_admin_notices->add(
18046 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18047 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18048 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18049 'error'
18050 );
18051 }
18052
18053 return;
18054 }
18055
18056 $addon_installs = $result->installs;
18057 foreach ( $addon_installs as $key => $addon_install ) {
18058 $addon_installs[ $key ] = new FS_Site( $addon_install );
18059 }
18060
18061 $first_install = $addon_installs[0];
18062
18063 // Get user information based on parent's plugin.
18064 $user = $parent_fs->get_user();
18065
18066 // First of all, set site and user info - otherwise we won't
18067 // be able to invoke API calls.
18068 $this->_site = $first_install;
18069 $this->_user = $user;
18070
18071 // Sync add-on plans.
18072 $this->_sync_plans();
18073
18074 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18075
18076 // Get site's current plan.
18077 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18078
18079 // Sync licenses.
18080 $this->_sync_licenses();
18081
18082 if ( ! fs_is_network_admin() ) {
18083 // Try to activate premium license.
18084 $this->_activate_license( true, $bundle_license );
18085
18086 if ( is_object( $bundle_license ) ) {
18087 $this->maybe_activate_bundle_license( $bundle_license );
18088 }
18089 } else {
18090 if ( is_object( $bundle_license ) ) {
18091 $premium_license = $bundle_license;
18092 } else {
18093 $license_id = fs_request_get( 'license_id' );
18094
18095 if ( is_object( $this->_site ) &&
18096 FS_Plugin_License::is_valid_id( $license_id ) &&
18097 $license_id == $this->_site->license_id
18098 ) {
18099 // License is already activated.
18100 return;
18101 }
18102
18103 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18104 $this->_get_license_by_id( $license_id ) :
18105 $this->_get_available_premium_license();
18106 }
18107
18108 if ( is_object( $premium_license ) ) {
18109 $this->maybe_network_activate_addon_license( $premium_license );
18110 }
18111 }
18112 }
18113
18114 /**
18115 * @author Leo Fajardo (@leorw)
18116 * @since 2.3.0
18117 *
18118 * @param FS_Site[] $installs
18119 * @param bool $is_site_level
18120 */
18121 private function handle_account_connection( $installs, $is_site_level ) {
18122 $first_install = $installs[0];
18123
18124 if ( $is_site_level ) {
18125 $this->_set_account( $this->_user, $first_install );
18126
18127 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18128 } else {
18129 $this->_store_user();
18130
18131 // Map site addresses to their blog IDs.
18132 $address_to_blog_map = $this->get_address_to_blog_map();
18133
18134 $first_blog_id = null;
18135 $blog_2_install_map = array();
18136 foreach ( $installs as $install ) {
18137 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18138 $blog_id = $address_to_blog_map[ $address ];
18139
18140 $this->_store_site( true, $blog_id, $install );
18141
18142 if ( is_null( $first_blog_id ) ) {
18143 $first_blog_id = $blog_id;
18144 }
18145
18146 $blog_2_install_map[ $blog_id ] = $install;
18147 }
18148
18149 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18150 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18151 ) {
18152 // Store network user.
18153 $this->_storage->network_user_id = $this->_user->id;
18154 }
18155
18156 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18157 $this->_storage->network_install_blog_id = $first_blog_id;
18158 }
18159
18160 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18161 // Super admin opted in for all sites in the network.
18162 $this->_storage->is_network_connected = true;
18163 }
18164
18165 $this->_store_licenses( false );
18166
18167 self::$_accounts->store();
18168
18169 // Don't sync the installs data on network upgrade
18170 if ( ! $this->network_upgrade_mode_completed() ) {
18171 $this->send_installs_update();
18172 }
18173
18174 $current_blog = get_current_blog_id();
18175
18176 foreach ( $blog_2_install_map as $blog_id => $install ) {
18177 $this->switch_to_blog( $blog_id );
18178
18179 $this->do_action( 'after_account_connection', $this->_user, $install );
18180 }
18181
18182 // Switch install context back to the first install.
18183 $this->switch_to_blog(
18184 $current_blog,
18185 $first_install,
18186 ( $this->_site->id != $first_install->id )
18187 );
18188
18189 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18190 }
18191 }
18192
18193 /**
18194 * Tries to activate parent account based on add-on's info.
18195 *
18196 * @author Vova Feldman (@svovaf)
18197 * @since 1.2.2.7
18198 *
18199 * @param Freemius $parent_fs
18200 */
18201 private function activate_parent_account( Freemius $parent_fs ) {
18202 if ( ! $this->is_addon() ) {
18203 // This is not an add-on.
18204 return;
18205 }
18206
18207 if ( $parent_fs->is_registered() ) {
18208 // Already activated.
18209 return;
18210 }
18211
18212 // Activate parent with add-on's user credentials.
18213 $parent_install = $this->get_api_user_scope()->call(
18214 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18215 'post',
18216 $parent_fs->get_install_data_for_api( array(
18217 'uid' => $parent_fs->get_anonymous_id(),
18218 ), false, false )
18219 );
18220
18221 if ( isset( $parent_install->error ) ) {
18222 $this->_admin_notices->add(
18223 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18224 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18225 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18226 'error'
18227 );
18228
18229 return;
18230 }
18231
18232 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18233
18234 if ( $parent_fs->is_pending_activation() ) {
18235 $parent_fs->clear_pending_activation_mode();
18236 }
18237
18238 // Get user information based on parent's plugin.
18239 $user = $this->get_user();
18240
18241 // First of all, set site info - otherwise we won't
18242 // be able to invoke API calls.
18243 $parent_fs->_site = new FS_Site( $parent_install );
18244 $parent_fs->_user = $user;
18245
18246 // Sync add-on plans.
18247 $parent_fs->_sync_plans();
18248
18249 $parent_fs->update_license_required_permissions_if_anonymous();
18250
18251 $parent_fs->_set_account( $user, $parent_fs->_site );
18252 }
18253
18254 #endregion
18255
18256 #----------------------------------------------------------------------------------
18257 #region Admin Menu Items
18258 #----------------------------------------------------------------------------------
18259
18260 private $_menu_items = array();
18261
18262 /**
18263 * @author Vova Feldman (@svovaf)
18264 * @since 1.2.1.8
18265 *
18266 * @return array
18267 */
18268 function get_menu_items() {
18269 return $this->_menu_items;
18270 }
18271
18272 /**
18273 * @author Vova Feldman (@svovaf)
18274 * @since 1.0.7
18275 *
18276 * @return string
18277 */
18278 function get_menu_slug() {
18279 return $this->_menu->get_slug();
18280 }
18281
18282 /**
18283 * @author Vova Feldman (@svovaf)
18284 * @since 1.0.9
18285 */
18286 function _prepare_admin_menu() {
18287 // if ( ! $this->is_on() ) {
18288 // return;
18289 // }
18290
18291 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18292 return;
18293 }
18294
18295 /**
18296 * When running from a site admin with a network activated module and the connection
18297 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18298 * site level settings.
18299 *
18300 * @author Vova Feldman (@svovaf)
18301 * @since 2.0.0
18302 */
18303 $should_hide_site_admin_settings = (
18304 $this->_is_network_active &&
18305 ! fs_is_network_admin() &&
18306 ! $this->is_delegated_connection() &&
18307 ! $this->is_anonymous() &&
18308 ! $this->is_registered()
18309 );
18310
18311 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18312
18313 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18314 $should_hide_site_admin_settings
18315 ) {
18316 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18317 } else {
18318 $this->do_action( fs_is_network_admin() ?
18319 'before_network_admin_menu_init' :
18320 'before_admin_menu_init'
18321 );
18322
18323 $this->add_menu_action();
18324
18325 $this->add_network_menu_when_missing();
18326
18327 $this->add_submenu_items();
18328 }
18329 }
18330
18331 /**
18332 * Admin dashboard menu items modifications.
18333 *
18334 * NOTE: admin_menu action executed before admin_init.
18335 *
18336 * @author Vova Feldman (@svovaf)
18337 * @since 1.0.7
18338 *
18339 */
18340 private function add_menu_action() {
18341 if ( $this->is_activation_mode() ) {
18342 if ( $this->show_opt_in_on_setting_page() ) {
18343 $this->override_plugin_menu_with_activation();
18344 } else {
18345 /**
18346 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18347 */
18348 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18349 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18350 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18351 fs_request_get_bool( 'pending_activation' )
18352 ) {
18353 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18354 }
18355 }
18356 } else {
18357 if ( ! $this->is_registered() ) {
18358 // If not registered try to install user.
18359 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18360 $this->_install_with_new_user();
18361 }
18362 } else if (
18363 fs_request_is_action( 'sync_user' ) &&
18364 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18365 ) {
18366 $this->_handle_account_user_sync();
18367 }
18368 }
18369 }
18370
18371 /**
18372 * @author Vova Feldman (@svovaf)
18373 * @since 1.0.1
18374 */
18375 function _redirect_on_clicked_menu_link() {
18376 $this->_logger->entrance();
18377
18378 $page = fs_request_get('page');
18379 $page = is_string($page) ? strtolower($page) : '';
18380
18381 $this->_logger->log( 'page = ' . $page );
18382
18383 foreach ( $this->_menu_items as $priority => $items ) {
18384 foreach ( $items as $item ) {
18385 if ( isset( $item['url'] ) ) {
18386 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18387 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18388
18389 fs_redirect( $item['url'] );
18390 }
18391 }
18392 }
18393 }
18394 }
18395
18396 /**
18397 * Remove plugin's all admin menu items & pages, and replace with activation page.
18398 *
18399 * @author Vova Feldman (@svovaf)
18400 * @since 1.0.1
18401 */
18402 private function override_plugin_menu_with_activation() {
18403 $this->_logger->entrance();
18404
18405 $hook = false;
18406
18407 if ( ! $this->has_settings_menu() ) {
18408 // Add the opt-in page without a menu item.
18409 $hook = FS_Admin_Menu_Manager::add_subpage(
18410 '',
18411 $this->get_plugin_name(),
18412 $this->get_plugin_name(),
18413 'manage_options',
18414 $this->_slug,
18415 array( &$this, '_connect_page_render' )
18416 );
18417 } else if ( $this->_menu->is_top_level() ) {
18418 if ( $this->_menu->is_override_exact() ) {
18419 // Make sure the current page is matching the activation page.
18420 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18421 return;
18422 }
18423 }
18424
18425 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18426
18427 if ( false === $hook ) {
18428 // Create new menu item just for the opt-in.
18429 $hook = FS_Admin_Menu_Manager::add_page(
18430 $this->get_plugin_name(),
18431 $this->get_plugin_name(),
18432 'manage_options',
18433 $this->_menu->get_slug(),
18434 array( &$this, '_connect_page_render' )
18435 );
18436 }
18437 } else {
18438 $menus = array( $this->_menu->get_parent_slug() );
18439
18440 if ( $this->_menu->is_override_exact() ) {
18441 // Make sure the current page is matching the activation page.
18442 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18443 return;
18444 }
18445 }
18446
18447 foreach ( $menus as $parent_slug ) {
18448 $hook = $this->_menu->override_submenu_action(
18449 $parent_slug,
18450 $this->_menu->get_raw_slug(),
18451 array( &$this, '_connect_page_render' )
18452 );
18453
18454 if ( false !== $hook ) {
18455 // Found plugin's submenu item.
18456 break;
18457 }
18458 }
18459 }
18460
18461 if ( $this->is_activation_page() ) {
18462 // Clean admin page from distracting content.
18463 self::_clean_admin_content_section();
18464 }
18465
18466 if ( false !== $hook ) {
18467 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18468 $this->_install_with_current_user();
18469 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18470 $this->_install_with_new_user();
18471 }
18472 }
18473 }
18474
18475 /**
18476 * If a plugin was network activated and connected but don't have a network
18477 * level settings, then add an artificial menu item for the Account and other
18478 * Freemius settings.
18479 *
18480 * @author Vova Feldman (@svovaf)
18481 * @since 2.0.0
18482 */
18483 private function add_network_menu_when_missing() {
18484 $this->_logger->entrance();
18485
18486 if ( ! $this->_is_network_active ) {
18487 // Plugin wasn't activated on the network level.
18488 return;
18489 }
18490
18491 if ( ! fs_is_network_admin() ) {
18492 // The context is not the network admin.
18493 return;
18494 }
18495
18496 if ( $this->_menu->has_network_menu() ) {
18497 // Plugin already has a network level menu.
18498 return;
18499 }
18500
18501 if ( $this->is_network_activation_mode() ) {
18502 /**
18503 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18504 * screen is being shown.
18505 *
18506 * @author Leo Fajardo (@leorw)
18507 */
18508 return;
18509 }
18510
18511 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18512 if ( $this->is_network_delegated_connection() ) {
18513 // Super-admin delegated the connection to the site admins.
18514 return;
18515 }
18516 }
18517
18518 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18519
18520 if ( $this->_menu->has_menu() ||
18521 ! $this->is_addon() ||
18522 $this->is_activation_mode()
18523 ) {
18524 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18525 $this->get_plugin_name(),
18526 $this->get_plugin_name(),
18527 'manage_options',
18528 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18529 );
18530 }
18531 } else {
18532 $this->_menu->add_subpage_and_update(
18533 $this->_menu->get_parent_slug(),
18534 $this->get_plugin_name(),
18535 $this->get_plugin_name(),
18536 'manage_options',
18537 $this->_menu->get_slug()
18538 );
18539 }
18540 }
18541
18542 /**
18543 * @author Leo Fajardo (@leorw)
18544 * @since 1.2.1
18545 *
18546 * return string
18547 */
18548 function get_top_level_menu_capability() {
18549 global $menu;
18550
18551 $top_level_menu_slug = $this->get_top_level_menu_slug();
18552
18553 foreach ( $menu as $menu_info ) {
18554 /**
18555 * The second element in the menu info array is the capability/role that has access to the menu and the
18556 * third element is the menu slug.
18557 */
18558 if ( $menu_info[2] === $top_level_menu_slug ) {
18559 return $menu_info[1];
18560 }
18561 }
18562
18563 return 'read';
18564 }
18565
18566 /**
18567 * @author Vova Feldman (@svovaf)
18568 * @since 1.0.0
18569 *
18570 * @return string
18571 */
18572 private function get_top_level_menu_slug() {
18573 return ( $this->is_addon() ?
18574 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18575 $this->_menu->get_top_level_menu_slug() );
18576 }
18577
18578 /**
18579 * @author Vova Feldman (@svovaf)
18580 * @since 1.2.2.7
18581 *
18582 * @return string
18583 */
18584 function get_pricing_cta_label() {
18585 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18586
18587 if ( $this->is_in_trial_promotion() &&
18588 ! $this->is_paying_or_trial()
18589 ) {
18590 // If running a trial promotion, modify the pricing to load the trial.
18591 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18592 } else if ( $this->is_paying() ) {
18593 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18594 }
18595
18596 return $label;
18597 }
18598
18599 /**
18600 * @author Vova Feldman (@svovaf)
18601 * @since 1.2.2.7
18602 *
18603 * @return bool
18604 */
18605 function is_pricing_page_visible() {
18606 return (
18607 // Has at least one paid plan.
18608 $this->has_paid_plan() &&
18609 // Didn't ask to hide the pricing page.
18610 $this->is_page_visible( 'pricing' ) &&
18611 // Don't have a valid active license or has more than one plan.
18612 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18613 );
18614 }
18615
18616 /**
18617 * @author Leo Fajardo (@leorw)
18618 * @since 2.3.0
18619 *
18620 * @param bool $is_activation_mode
18621 *
18622 * @return bool
18623 */
18624 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18625 if ( $this->is_addon() ) {
18626 // No submenu items or action links for add-ons.
18627 return false;
18628 }
18629
18630 if ( $this->show_opt_in_on_themes_page() ) {
18631 if ( ! fs_is_network_admin() ) {
18632 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18633 return true;
18634 }
18635 } else if ( $is_activation_mode ) {
18636 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18637 return false;
18638 }
18639
18640 if ( fs_is_network_admin() ) {
18641 /**
18642 * Add submenu items or action links to network level when plugin was network activated and the super
18643 * admin did NOT delegate the connection of all sites to site admins.
18644 */
18645 return (
18646 $this->_is_network_active &&
18647 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18648 ! $this->is_network_delegated_connection() )
18649 );
18650 }
18651
18652 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18653 }
18654
18655 /**
18656 * Add default Freemius menu items.
18657 *
18658 * @author Vova Feldman (@svovaf)
18659 * @since 1.0.0
18660 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18661 */
18662 private function add_submenu_items() {
18663 $this->_logger->entrance();
18664
18665 $is_activation_mode = $this->is_activation_mode();
18666
18667 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18668
18669 if ( $add_submenu_items ) {
18670 if ( $this->has_affiliate_program() ) {
18671 // Add affiliation page.
18672 $this->add_submenu_item(
18673 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18674 array( &$this, '_affiliation_page_render' ),
18675 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18676 'manage_options',
18677 'affiliation',
18678 'Freemius::_clean_admin_content_section',
18679 WP_FS__DEFAULT_PRIORITY,
18680 $this->is_submenu_item_visible( 'affiliation' )
18681 );
18682 }
18683 }
18684
18685 if ( $add_submenu_items ||
18686 ( $is_activation_mode &&
18687 $this->is_only_premium() &&
18688 $this->is_admin_page( 'account' ) &&
18689 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18690 )
18691 ) {
18692 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18693 $show_account = (
18694 $this->is_submenu_item_visible( 'account' ) &&
18695 /**
18696 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18697 */
18698 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18699 );
18700
18701 // Add user account page.
18702 $this->add_submenu_item(
18703 $this->get_text_inline( 'Account', 'account' ),
18704 array( &$this, '_account_page_render' ),
18705 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18706 'manage_options',
18707 'account',
18708 array( &$this, '_account_page_load' ),
18709 WP_FS__DEFAULT_PRIORITY,
18710 ( $add_submenu_items && $show_account )
18711 );
18712 }
18713 }
18714
18715 if ( $add_submenu_items ) {
18716 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18717 // Add contact page.
18718 if ( $this->is_premium() ) {
18719 $this->add_submenu_item(
18720 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18721 array( &$this, '_contact_page_render' ),
18722 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18723 'manage_options',
18724 'contact',
18725 'Freemius::_clean_admin_content_section',
18726 WP_FS__DEFAULT_PRIORITY,
18727 $this->is_submenu_item_visible( 'contact' )
18728 );
18729 } else {
18730 $this->add_submenu_link_item(
18731 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18732 FS_Contact_Form_Manager::instance()->get_standalone_link( $this ),
18733 'contact',
18734 'manage_options',
18735 WP_FS__DEFAULT_PRIORITY,
18736 $this->is_submenu_item_visible( 'contact' ),
18737 'fs_external_contact',
18738 true
18739 );
18740 }
18741 }
18742
18743 if ( $this->has_addons() ) {
18744 $this->add_submenu_item(
18745 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18746 array( &$this, '_addons_page_render' ),
18747 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18748 'manage_options',
18749 'addons',
18750 array( &$this, '_addons_page_load' ),
18751 WP_FS__LOWEST_PRIORITY - 1,
18752 $this->is_submenu_item_visible( 'addons' )
18753 );
18754 }
18755 }
18756
18757 if ( $add_submenu_items ||
18758 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18759 ) {
18760 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18761 $show_pricing = (
18762 $this->is_submenu_item_visible( 'pricing' ) &&
18763 $this->is_pricing_page_visible()
18764 );
18765
18766 $pricing_cta_text = $this->get_pricing_cta_label();
18767 $pricing_class = 'upgrade-mode';
18768 if ( $show_pricing ) {
18769 if ( $this->is_in_trial_promotion() &&
18770 ! $this->is_paying_or_trial()
18771 ) {
18772 // If running a trial promotion, modify the pricing to load the trial.
18773 $pricing_class = 'trial-mode';
18774 } else if ( $this->is_paying() ) {
18775 $pricing_class = '';
18776 }
18777 }
18778
18779 $custom_pricing_url = $this->get_pricing_url_with_filter( null );
18780 $pricing_menu_title = $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' ) );
18781 $show_pricing_submenu_item = ( $add_submenu_items && $show_pricing );
18782
18783 // Add upgrade/pricing submenu item.
18784 if ( ! is_null( $custom_pricing_url ) ) {
18785 $this->add_submenu_link_item(
18786 $pricing_menu_title,
18787 $custom_pricing_url,
18788 'pricing',
18789 'manage_options',
18790 WP_FS__LOWEST_PRIORITY,
18791 $show_pricing_submenu_item,
18792 $pricing_class
18793 );
18794 } else {
18795 $this->add_submenu_item(
18796 $pricing_menu_title,
18797 array( &$this, '_pricing_page_render' ),
18798 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
18799 'manage_options',
18800 'pricing',
18801 'Freemius::_clean_admin_content_section',
18802 WP_FS__LOWEST_PRIORITY,
18803 $show_pricing_submenu_item,
18804 $pricing_class
18805 );
18806 }
18807 }
18808 }
18809
18810 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
18811 /**
18812 * Add the other menu items if there are any when not in activation mode or license activation is not
18813 * required (license activation is required for registered or anonymous users after activating the
18814 * premium version when the site is not in trial mode or there's no active valid license).
18815 *
18816 * @author Leo Fajardo (@leorw)
18817 * @since 2.2.1
18818 */
18819 if ( 0 < count( $this->_menu_items ) ) {
18820 if ( ! $this->_menu->is_top_level() ) {
18821 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
18822
18823 // Append submenu items right after the plugin's submenu item.
18824 $this->order_sub_submenu_items();
18825 } else {
18826 // Append submenu items.
18827 $this->embed_submenu_items();
18828 }
18829 }
18830 }
18831 }
18832
18833 /**
18834 * Moved the actual submenu item additions to a separated function,
18835 * in order to support sub-submenu items when the plugin's settings
18836 * only have a submenu and not top-level menu item.
18837 *
18838 * @author Vova Feldman (@svovaf)
18839 * @since 1.1.4
18840 */
18841 private function embed_submenu_items() {
18842 $item_classes = $this->_menu->is_top_level() ? 'fs-submenu-item' : 'fs-submenu-item fs-sub';
18843
18844 $item_template = '<span class="' . $item_classes . ' %1$s %2$s %3$s" data-fs-external-url="%5$s" data-fs-new-tab="%6$s">%4$s</span>';
18845
18846 $top_level_menu_capability = $this->get_top_level_menu_capability();
18847
18848 ksort( $this->_menu_items );
18849
18850 $is_first_submenu_item = true;
18851
18852 foreach ( $this->_menu_items as $priority => $items ) {
18853 foreach ( $items as $item ) {
18854 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
18855
18856 $menu_item = sprintf(
18857 $item_template,
18858 $this->get_unique_affix(),
18859 $item['menu_slug'],
18860 ! empty( $item['class'] ) ? $item['class'] : '',
18861 $item['menu_title'],
18862 esc_attr( isset( $item['url'] ) ? $item['url'] : '' ),
18863 esc_attr( isset( $item['new_tab'] ) ? 'true' : 'false' )
18864 );
18865
18866 $top_level_menu_slug = $this->get_top_level_menu_slug();
18867 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
18868
18869 if ( ! isset( $item['url'] ) ) {
18870 $hook = FS_Admin_Menu_Manager::add_subpage(
18871 $item['show_submenu'] ?
18872 $top_level_menu_slug :
18873 '',
18874 $item['page_title'],
18875 $menu_item,
18876 $capability,
18877 $menu_slug,
18878 $item['render_function']
18879 );
18880
18881 if ( false !== $item['before_render_function'] ) {
18882 add_action( "load-$hook", $item['before_render_function'] );
18883 }
18884 } else {
18885 FS_Admin_Menu_Manager::add_subpage(
18886 $item['show_submenu'] ?
18887 $top_level_menu_slug :
18888 '',
18889 $item['page_title'],
18890 $menu_item,
18891 $capability,
18892 $menu_slug,
18893 array( $this, '' )
18894 );
18895 }
18896
18897 if ( $item['show_submenu'] && $is_first_submenu_item ) {
18898 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
18899 /**
18900 * If the top-level menu has been dynamically created, remove the first submenu item that
18901 * WordPress automatically creates when there's no submenu item whose slug matches the
18902 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
18903 *
18904 * Awesome Plugin
18905 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
18906 *
18907 * @author Leo Fajardo (@leorw)
18908 */
18909 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
18910 }
18911
18912 $is_first_submenu_item = false;
18913 }
18914 }
18915 }
18916 }
18917
18918 /**
18919 * Re-order the submenu items so all Freemius added new submenu items
18920 * are added right after the plugin's settings submenu item.
18921 *
18922 * @author Vova Feldman (@svovaf)
18923 * @since 1.1.4
18924 */
18925 private function order_sub_submenu_items() {
18926 global $submenu;
18927
18928 $menu_slug = $this->_menu->get_top_level_menu_slug();
18929
18930 /**
18931 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
18932 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
18933 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
18934 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
18935 *
18936 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
18937 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
18938 * capability needed to access the parent menu as the capability for the submenus that we will add.
18939 */
18940 if ( empty( $submenu[ $menu_slug ] ) ) {
18941 return;
18942 }
18943
18944 $top_level_menu = &$submenu[ $menu_slug ];
18945
18946 $all_submenu_items_after = array();
18947
18948 $found_submenu_item = false;
18949
18950 foreach ( $top_level_menu as $submenu_id => $meta ) {
18951 if ( $found_submenu_item ) {
18952 // Remove all submenu items after the plugin's submenu item.
18953 $all_submenu_items_after[] = $meta;
18954 unset( $top_level_menu[ $submenu_id ] );
18955 }
18956
18957 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
18958 // Found the submenu item, put all below.
18959 $found_submenu_item = true;
18960 continue;
18961 }
18962 }
18963
18964 // Embed all plugin's new submenu items.
18965 $this->embed_submenu_items();
18966
18967 // Start with specially high number to make sure it's appended.
18968 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
18969 foreach ( $all_submenu_items_after as $meta ) {
18970 $top_level_menu[ $i ] = $meta;
18971 $i ++;
18972 }
18973
18974 // Sort submenu items.
18975 ksort( $top_level_menu );
18976 }
18977
18978 /**
18979 * Helper method to return the module's support forum URL.
18980 *
18981 * @author Vova Feldman (@svovaf)
18982 * @since 1.2.2.7
18983 *
18984 * @return string
18985 */
18986 function get_support_forum_url() {
18987 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
18988 }
18989
18990 /**
18991 * Displays the Support Forum link when enabled.
18992 *
18993 * Can be filtered like so:
18994 *
18995 * function _fs_show_support_menu( $is_visible, $menu_id ) {
18996 * if ( 'support' === $menu_id ) {
18997 * return _fs->is_registered();
18998 * }
18999 * return $is_visible;
19000 * }
19001 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19002 *
19003 */
19004 function _add_default_submenu_items() {
19005 if ( ! $this->is_on() ) {
19006 return;
19007 }
19008
19009 if ( ! $this->is_activation_mode() &&
19010 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19011 ( ! $this->_is_network_active && is_admin() ) )
19012 ) {
19013 $this->add_submenu_link_item(
19014 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19015 $this->get_support_forum_url(),
19016 'wp-support-forum',
19017 null,
19018 50,
19019 $this->is_submenu_item_visible( 'support' ),
19020 '',
19021 true
19022 );
19023 }
19024 }
19025
19026 /**
19027 * @author Vova Feldman (@svovaf)
19028 * @since 1.0.1
19029 *
19030 * @param string $menu_title
19031 * @param callable $render_function
19032 * @param bool|string $page_title
19033 * @param string $capability
19034 * @param bool|string $menu_slug
19035 * @param bool|callable $before_render_function
19036 * @param int $priority
19037 * @param bool $show_submenu
19038 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19039 */
19040 function add_submenu_item(
19041 $menu_title,
19042 $render_function,
19043 $page_title = false,
19044 $capability = 'manage_options',
19045 $menu_slug = false,
19046 $before_render_function = false,
19047 $priority = WP_FS__DEFAULT_PRIORITY,
19048 $show_submenu = true,
19049 $class = ''
19050 ) {
19051 $this->_logger->entrance( 'Title = ' . $menu_title );
19052
19053 if ( $this->is_addon() ) {
19054 $parent_fs = $this->get_parent_instance();
19055
19056 if ( is_object( $parent_fs ) ) {
19057 $parent_fs->add_submenu_item(
19058 $menu_title,
19059 $render_function,
19060 $page_title,
19061 $capability,
19062 $menu_slug,
19063 $before_render_function,
19064 $priority,
19065 $show_submenu,
19066 $class
19067 );
19068
19069 return;
19070 }
19071 }
19072
19073 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19074 $this->_menu_items[ $priority ] = array();
19075 }
19076
19077 $this->_menu_items[ $priority ][] = array(
19078 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19079 'menu_title' => $menu_title,
19080 'capability' => $capability,
19081 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19082 'render_function' => $render_function,
19083 'before_render_function' => $before_render_function,
19084 'show_submenu' => $show_submenu,
19085 'class' => $class,
19086 );
19087 }
19088
19089 /**
19090 * @author Vova Feldman (@svovaf)
19091 * @since 1.0.1
19092 *
19093 * @param string $menu_title
19094 * @param string $url
19095 * @param bool $menu_slug
19096 * @param string $capability
19097 * @param int $priority
19098 * @param bool $show_submenu
19099 * @param string $class
19100 * @param bool $new_tab
19101 */
19102 function add_submenu_link_item(
19103 $menu_title,
19104 $url,
19105 $menu_slug = false,
19106 $capability = 'read',
19107 $priority = WP_FS__DEFAULT_PRIORITY,
19108 $show_submenu = true,
19109 $class = '',
19110 $new_tab = false
19111 ) {
19112 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19113
19114 if ( $this->is_addon() ) {
19115 $parent_fs = $this->get_parent_instance();
19116
19117 if ( is_object( $parent_fs ) ) {
19118 $parent_fs->add_submenu_link_item(
19119 $menu_title,
19120 $url,
19121 $menu_slug,
19122 $capability,
19123 $priority,
19124 $show_submenu,
19125 $class,
19126 $new_tab
19127 );
19128
19129 return;
19130 }
19131 }
19132
19133 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19134 $this->_menu_items[ $priority ] = array();
19135 }
19136
19137 $this->_menu_items[ $priority ][] = array(
19138 'menu_title' => $menu_title,
19139 'capability' => $capability,
19140 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19141 'url' => $url,
19142 'page_title' => $menu_title,
19143 'render_function' => 'fs_dummy',
19144 'before_render_function' => '',
19145 'show_submenu' => $show_submenu,
19146 'class' => $class,
19147 'new_tab' => $new_tab,
19148 );
19149 }
19150
19151 #endregion ------------------------------------------------------------------
19152
19153 #--------------------------------------------------------------------------------
19154 #region Admin Notices
19155 #--------------------------------------------------------------------------------
19156
19157 /**
19158 * @author Vova Feldman (@svovaf)
19159 * @since 2.3.1
19160 *
19161 * @param string|string[] $ids
19162 * @param int|null $network_level_or_blog_id
19163 *
19164 * @uses FS_Admin_Notices::remove_sticky()
19165 */
19166 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19167 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19168 }
19169
19170 #endregion
19171
19172 #--------------------------------------------------------------------------------
19173 #region Actions / Hooks / Filters
19174 #--------------------------------------------------------------------------------
19175
19176 /**
19177 * @author Vova Feldman (@svovaf)
19178 * @since 1.1.7
19179 *
19180 * @param string $tag
19181 *
19182 * @return string
19183 */
19184 public function get_action_tag( $tag ) {
19185 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19186 }
19187
19188 /**
19189 * @author Vova Feldman (@svovaf)
19190 * @since 1.2.1.6
19191 *
19192 * @param string $tag
19193 * @param string $slug
19194 * @param bool $is_plugin
19195 *
19196 * @return string
19197 */
19198 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19199 $action = "fs_{$tag}";
19200
19201 if ( ! empty( $slug ) ) {
19202 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19203 }
19204
19205 return $action;
19206 }
19207
19208 /**
19209 * Returns a string that can be used to generate a unique action name,
19210 * option name, HTML element ID, or HTML element class.
19211 *
19212 * @author Leo Fajardo (@leorw)
19213 * @since 1.2.2
19214 *
19215 * @return string
19216 */
19217 public function get_unique_affix() {
19218 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19219 }
19220
19221 /**
19222 * Returns a string that can be used to generate a unique action name,
19223 * option name, HTML element ID, or HTML element class.
19224 *
19225 * @author Vova Feldman (@svovaf)
19226 * @since 1.2.2.5
19227 *
19228 * @param string $slug
19229 * @param bool $is_plugin
19230 *
19231 * @return string
19232 */
19233 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19234 $affix = $slug;
19235
19236 if ( ! $is_plugin ) {
19237 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19238 }
19239
19240 return $affix;
19241 }
19242
19243 /**
19244 * @author Vova Feldman (@svovaf)
19245 * @since 1.2.1
19246 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19247 * based on the slug for backward compatibility.
19248 *
19249 * @param string $tag
19250 *
19251 * @return string
19252 */
19253 function get_ajax_action( $tag ) {
19254 return self::get_ajax_action_static( $tag, $this->_module_id );
19255 }
19256
19257 /**
19258 * @author Vova Feldman (@svovaf)
19259 * @since 1.2.1.7
19260 *
19261 * @param string $tag
19262 *
19263 * @return string
19264 */
19265 function get_ajax_security( $tag ) {
19266 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19267 }
19268
19269 /**
19270 * @author Vova Feldman (@svovaf)
19271 * @since 1.2.1.7
19272 *
19273 * @param string $tag
19274 */
19275 function check_ajax_referer( $tag ) {
19276 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19277 }
19278
19279 /**
19280 * @author Vova Feldman (@svovaf)
19281 * @since 1.2.1.6
19282 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19283 * based on the slug for backward compatibility.
19284 *
19285 * @param string $tag
19286 * @param number|null $module_id
19287 *
19288 * @return string
19289 */
19290 static function get_ajax_action_static( $tag, $module_id = null ) {
19291 $action = "fs_{$tag}";
19292
19293 if ( ! empty( $module_id ) ) {
19294 $action .= "_{$module_id}";
19295 }
19296
19297 return $action;
19298 }
19299
19300 /**
19301 * Do action, specific for the current context plugin.
19302 *
19303 * @author Vova Feldman (@svovaf)
19304 * @since 1.0.1
19305 *
19306 * @param string $tag The name of the action to be executed.
19307 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19308 * functions hooked to the action. Default empty.
19309 *
19310 * @uses do_action()
19311 */
19312 function do_action( $tag, $arg = '' ) {
19313 $args = func_get_args();
19314
19315 $this->_logger->entrance( $tag );
19316
19317 call_user_func_array( 'do_action', array_merge(
19318 array( $this->get_action_tag( $tag ) ),
19319 array_slice( $args, 1 ) )
19320 );
19321 }
19322
19323 /**
19324 * Add action, specific for the current context plugin.
19325 *
19326 * @author Vova Feldman (@svovaf)
19327 * @since 1.0.1
19328 *
19329 * @param string $tag
19330 * @param callable $function_to_add
19331 * @param int $priority
19332 * @param int $accepted_args
19333 *
19334 * @uses add_action()
19335 */
19336 function add_action(
19337 $tag,
19338 $function_to_add,
19339 $priority = WP_FS__DEFAULT_PRIORITY,
19340 $accepted_args = 1
19341 ) {
19342 $this->_logger->entrance( $tag );
19343
19344 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19345 }
19346
19347 /**
19348 * Add AJAX action, specific for the current context plugin.
19349 *
19350 * @author Vova Feldman (@svovaf)
19351 * @since 1.2.1
19352 *
19353 * @param string $tag
19354 * @param callable $function_to_add
19355 * @param int $priority
19356 *
19357 * @uses add_action()
19358 *
19359 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19360 */
19361 function add_ajax_action(
19362 $tag,
19363 $function_to_add,
19364 $priority = WP_FS__DEFAULT_PRIORITY
19365 ) {
19366 $this->_logger->entrance( $tag );
19367
19368 return self::add_ajax_action_static(
19369 $tag,
19370 $function_to_add,
19371 $priority,
19372 $this->_module_id
19373 );
19374 }
19375
19376 /**
19377 * Add AJAX action.
19378 *
19379 * @author Vova Feldman (@svovaf)
19380 * @since 1.2.1.6
19381 *
19382 * @param string $tag
19383 * @param callable $function_to_add
19384 * @param int $priority
19385 * @param number|null $module_id
19386 *
19387 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19388 * @uses add_action()
19389 *
19390 */
19391 static function add_ajax_action_static(
19392 $tag,
19393 $function_to_add,
19394 $priority = WP_FS__DEFAULT_PRIORITY,
19395 $module_id = null
19396 ) {
19397 self::$_static_logger->entrance( $tag );
19398
19399 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19400 return false;
19401 }
19402
19403 add_action(
19404 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19405 $function_to_add,
19406 $priority,
19407 0
19408 );
19409
19410 self::$_static_logger->info( "$tag AJAX callback action added." );
19411
19412 return true;
19413 }
19414
19415 /**
19416 * Send a JSON response back to an Ajax request.
19417 *
19418 * @author Vova Feldman (@svovaf)
19419 * @since 1.2.1.5
19420 *
19421 * @param mixed $response
19422 */
19423 static function shoot_ajax_response( $response ) {
19424 wp_send_json( $response );
19425 }
19426
19427 /**
19428 * Send a JSON response back to an Ajax request, indicating success.
19429 *
19430 * @author Vova Feldman (@svovaf)
19431 * @since 1.2.1.5
19432 *
19433 * @param mixed $data Data to encode as JSON, then print and exit.
19434 */
19435 static function shoot_ajax_success( $data = null ) {
19436 wp_send_json_success( $data );
19437 }
19438
19439 /**
19440 * Send a JSON response back to an Ajax request, indicating failure.
19441 *
19442 * @author Vova Feldman (@svovaf)
19443 * @since 1.2.1.5
19444 *
19445 * @param mixed $error Optional error message.
19446 */
19447 static function shoot_ajax_failure( $error = '' ) {
19448 $result = array( 'success' => false );
19449 if ( ! empty( $error ) ) {
19450 $result['error'] = $error;
19451 }
19452
19453 wp_send_json( $result );
19454 }
19455
19456 /**
19457 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19458 *
19459 * @author Vova Feldman (@svovaf)
19460 * @since 2.5.1
19461 *
19462 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19463 *
19464 * @return string
19465 */
19466 static function ajax_url( $wrap_with = "'") {
19467 if ( fs_is_network_admin() ) {
19468 $param_name = '_fs_network_admin';
19469 } else {
19470 $param_name = '_fs_blog_admin';
19471 }
19472
19473 $url = admin_url( 'admin-ajax.php', 'relative' );
19474 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19475 $url .= "{$param_name}=true";
19476
19477 return "{$wrap_with}{$url}{$wrap_with}";
19478 }
19479
19480 /**
19481 * Apply filter, specific for the current context plugin.
19482 *
19483 * @author Vova Feldman (@svovaf)
19484 * @since 1.0.9
19485 *
19486 * @param string $tag The name of the filter hook.
19487 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19488 *
19489 * @return mixed The filtered value after all hooked functions are applied to it.
19490 *
19491 * @uses apply_filters()
19492 */
19493 function apply_filters( $tag, $value ) {
19494 $args = func_get_args();
19495
19496 $this->_logger->entrance( $tag );
19497
19498 array_unshift( $args, $this->get_unique_affix() );
19499
19500 return call_user_func_array( 'fs_apply_filter', $args );
19501 }
19502
19503 /**
19504 * Add filter, specific for the current context plugin.
19505 *
19506 * @author Vova Feldman (@svovaf)
19507 * @since 1.0.9
19508 *
19509 * @param string $tag
19510 * @param callable $function_to_add
19511 * @param int $priority
19512 * @param int $accepted_args
19513 *
19514 * @uses add_filter()
19515 */
19516 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19517 $this->_logger->entrance( $tag );
19518
19519 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19520 }
19521
19522 /**
19523 * Check if has filter.
19524 *
19525 * @author Vova Feldman (@svovaf)
19526 * @since 1.1.4
19527 *
19528 * @param string $tag
19529 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19530 *
19531 * @return false|int
19532 *
19533 * @uses has_filter()
19534 */
19535 function has_filter( $tag, $function_to_check = false ) {
19536 $this->_logger->entrance( $tag );
19537
19538 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19539 }
19540
19541 #endregion
19542
19543 /**
19544 * Override default i18n text phrases.
19545 *
19546 * @author Vova Feldman (@svovaf)
19547 * @since 1.1.6
19548 *
19549 * @param string[] string $key_value
19550 *
19551 * @uses fs_override_i18n()
19552 */
19553 function override_i18n( $key_value ) {
19554 fs_override_i18n( $key_value, $this->_slug );
19555 }
19556
19557 /* Account Page
19558 ------------------------------------------------------------------------------------------------------------------*/
19559 /**
19560 * Update site information.
19561 *
19562 * @author Vova Feldman (@svovaf)
19563 * @since 1.0.1
19564 *
19565 * @param bool $store Flush to Database if true.
19566 * @param null|int $network_level_or_blog_id Since 2.0.0
19567 * @param \FS_Site $site Since 2.0.0
19568 */
19569 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
19570 $this->_logger->entrance();
19571
19572 if ( is_null( $site ) ) {
19573 $site = $this->_site;
19574 }
19575
19576 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19577 $this->_logger->error( "Empty install ID, can't store site." );
19578
19579 return;
19580 }
19581
19582 $site_clone = clone $site;
19583
19584 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19585
19586 if (
19587 ! $is_backup &&
19588 is_object( $this->_user ) && $this->_user->id != $site->user_id
19589 ) {
19590 $this->sync_user_by_current_install( $site->user_id );
19591
19592 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19593
19594 if ( empty( $prev_stored_user_id ) &&
19595 is_object($this->_user) && $this->_user->id != $site->user_id
19596 ) {
19597 /**
19598 * Store the current user ID as the previous user ID so that the previous user can be used
19599 * as the install's owner while the new owner's details are not yet available.
19600 *
19601 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19602 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19603 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19604 * into `replica`.
19605 *
19606 * @author Leo Fajardo (@leorw)
19607 */
19608 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19609 }
19610 }
19611
19612 $sites[ $this->_slug ] = $site_clone;
19613
19614 $this->set_account_option(
19615 ( $is_backup ? 'prev_' : '' ) . 'sites',
19616 $sites,
19617 $store,
19618 $network_level_or_blog_id
19619 );
19620 }
19621
19622 /**
19623 * Stores the context site in the sites backup storage. This logic is used before deleting the site info so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails).
19624 *
19625 * @author Leo Fajardo (@leorw)
19626 * @since 2.5.0
19627 */
19628 private function back_up_site() {
19629 $this->_logger->entrance();
19630
19631 $site_clone = clone $this->_site;
19632
19633 $this->_store_site( true, null, $site_clone, true );
19634 }
19635
19636 /**
19637 * Update plugin's plans information.
19638 *
19639 * @author Vova Feldman (@svovaf)
19640 * @since 1.0.2
19641 *
19642 * @param bool $store Flush to Database if true.
19643 */
19644 private function _store_plans( $store = true ) {
19645 $this->_logger->entrance();
19646
19647 $plans = self::get_all_plans( $this->_module_type );
19648
19649 // Copy plans.
19650 $encrypted_plans = array();
19651 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19652 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19653 }
19654
19655 $plans[ $this->_slug ] = $encrypted_plans;
19656
19657 $this->set_account_option( 'plans', $plans, $store );
19658 }
19659
19660 /**
19661 * Update user's plugin licenses.
19662 *
19663 * @author Vova Feldman (@svovaf)
19664 * @since 1.0.5
19665 *
19666 * @param bool $store
19667 * @param number|bool $module_id
19668 * @param FS_Plugin_License[] $licenses
19669 */
19670 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19671 $this->_logger->entrance();
19672
19673 $all_licenses = self::get_all_licenses();
19674
19675 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19676 $module_id = $this->_module_id;
19677
19678 $user_licenses = is_array( $this->_licenses ) ?
19679 $this->_licenses :
19680 array();
19681
19682 if ( empty( $user_licenses ) ) {
19683 // If the context user doesn't have any license, don't update the licenses collection.
19684 return;
19685 }
19686
19687 $new_user_licenses_map = array();
19688 foreach ( $user_licenses as $user_license ) {
19689 $new_user_licenses_map[ $user_license->id ] = $user_license;
19690 }
19691
19692 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19693
19694 // Update user licenses.
19695 $licenses_to_update_count = count( $new_user_licenses_map );
19696 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19697 if ( 0 === $licenses_to_update_count ) {
19698 break;
19699 }
19700
19701 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19702 // Update license.
19703 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19704 unset( $new_user_licenses_map[ $license->id ] );
19705
19706 $licenses_to_update_count --;
19707 }
19708 }
19709
19710 if ( ! empty( $new_user_licenses_map ) ) {
19711 // Add new licenses.
19712 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19713 }
19714
19715 $licenses = $all_licenses[ $module_id ];
19716 }
19717
19718 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19719 $all_licenses[ $module_id ] = array();
19720 }
19721
19722 $all_licenses[ $module_id ] = $licenses;
19723
19724 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19725 }
19726
19727 /**
19728 * Update user information.
19729 *
19730 * @author Vova Feldman (@svovaf)
19731 * @since 1.0.1
19732 *
19733 * @param bool $store Flush to Database if true.
19734 */
19735 private function _store_user( $store = true ) {
19736 $this->_logger->entrance();
19737
19738 if ( empty( $this->_user->id ) ) {
19739 $this->_logger->error( "Empty user ID, can't store user." );
19740
19741 return;
19742 }
19743
19744 $users = self::get_all_users();
19745 $users[ $this->_user->id ] = $this->_user;
19746 self::$_accounts->set_option( 'users', $users, $store );
19747 }
19748
19749 /**
19750 * Update new updates information.
19751 *
19752 * @author Vova Feldman (@svovaf)
19753 * @since 1.0.4
19754 *
19755 * @param FS_Plugin_Tag|null $update
19756 * @param bool $store Flush to Database if true.
19757 * @param bool|number $plugin_id
19758 */
19759 private function _store_update( $update, $store = true, $plugin_id = false ) {
19760 $this->_logger->entrance();
19761
19762 if ( $update instanceof FS_Plugin_Tag ) {
19763 $update->updated = time();
19764 }
19765
19766 if ( ! is_numeric( $plugin_id ) ) {
19767 $plugin_id = $this->_plugin->id;
19768 }
19769
19770 $updates = self::get_all_updates();
19771 $updates[ $plugin_id ] = $update;
19772 self::$_accounts->set_option( 'updates', $updates, $store );
19773 }
19774
19775 /**
19776 * Update new updates information.
19777 *
19778 * @author Vova Feldman (@svovaf)
19779 * @since 1.0.6
19780 *
19781 * @param FS_Plugin[] $plugin_addons
19782 * @param bool $store Flush to Database if true.
19783 */
19784 private function _store_addons( $plugin_addons, $store = true ) {
19785 $this->_logger->entrance();
19786
19787 $addons = self::get_all_addons();
19788 $addons[ $this->_plugin->id ] = $plugin_addons;
19789 self::$_accounts->set_option( 'addons', $addons, $store );
19790 }
19791
19792 /**
19793 * Delete plugin's associated add-ons.
19794 *
19795 * @author Vova Feldman (@svovaf)
19796 * @since 1.0.8
19797 *
19798 * @param bool $store
19799 *
19800 * @return bool
19801 */
19802 private function _delete_account_addons( $store = true ) {
19803 $all_addons = self::get_all_account_addons();
19804
19805 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
19806 return false;
19807 }
19808
19809 unset( $all_addons[ $this->_plugin->id ] );
19810
19811 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19812
19813 return true;
19814 }
19815
19816 /**
19817 * Update account add-ons list.
19818 *
19819 * @author Vova Feldman (@svovaf)
19820 * @since 1.0.6
19821 *
19822 * @param FS_Plugin[] $addons
19823 * @param bool $store Flush to Database if true.
19824 */
19825 private function _store_account_addons( $addons, $store = true ) {
19826 $this->_logger->entrance();
19827
19828 $all_addons = self::get_all_account_addons();
19829 $all_addons[ $this->_plugin->id ] = $addons;
19830 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
19831 }
19832
19833 /**
19834 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
19835 * the valid user licenses will be fetched again and the account add-ons may be updated.
19836 *
19837 * @author Leo Fajardo (@leorw)
19838 * @since 2.2.4
19839 */
19840 private function purge_valid_user_licenses_cache() {
19841 if ( ! $this->is_registered() ) {
19842 return;
19843 }
19844
19845 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
19846 }
19847
19848 /**
19849 * @author Leo Fajardo (@leorw)
19850 * @since 2.3.0
19851 *
19852 * @param array $all_licenses
19853 * @param number|null $site_license_id
19854 * @param bool $include_parent_licenses
19855 *
19856 * @return array
19857 */
19858 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
19859 $foreign_licenses = array(
19860 'ids' => array(),
19861 'license_keys' => array()
19862 );
19863
19864 $parent_license_ids_map = array();
19865
19866 foreach ( $all_licenses as $license ) {
19867 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
19868 continue;
19869 }
19870
19871 $foreign_licenses['ids'][] = $license->id;
19872 $foreign_licenses['license_keys'][] = $license->secret_key;
19873
19874 if (
19875 $include_parent_licenses &&
19876 is_object( $this->_license ) &&
19877 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
19878 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
19879 ) {
19880 /**
19881 * Include the parent license's info only if it has not been included before since child licenses
19882 * can have the same parent license.
19883 */
19884 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
19885 $foreign_licenses['license_keys'][] = $license->secret_key;
19886
19887 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
19888 }
19889 }
19890
19891 if ( empty( $foreign_licenses['ids'] ) ) {
19892 $foreign_licenses = array();
19893 }
19894
19895 return $foreign_licenses;
19896 }
19897
19898 /**
19899 * @author Leo Fajardo (@leorw)
19900 * @since 2.3.0
19901 *
19902 * @return string
19903 */
19904 private function get_valid_user_licenses_endpoint() {
19905 $user_licenses_endpoint = '/licenses.json?type=active' .
19906 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
19907
19908 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
19909
19910 if ( ! empty ( $foreign_licenses ) ) {
19911 $foreign_licenses = array(
19912 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
19913 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
19914 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
19915 );
19916
19917 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
19918 }
19919
19920 return $user_licenses_endpoint;
19921 }
19922
19923 /**
19924 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
19925 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
19926 * method filters out non–add-on product IDs and stores the add-on IDs.
19927 *
19928 * @author Leo Fajardo (@leorw)
19929 * @since 2.2.4
19930 *
19931 * @return stdClass[] array
19932 */
19933 private function fetch_valid_user_licenses() {
19934 $this->_logger->entrance();
19935
19936 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
19937
19938 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
19939 ! is_array( $result->licenses )
19940 ) {
19941 return array();
19942 }
19943
19944 return $result->licenses;
19945 }
19946
19947 /**
19948 * @author Leo Fajardo (@leorw)
19949 * @since 2.2.4
19950 *
19951 * @return number[] Account add-on IDs.
19952 */
19953 function get_updated_account_addons() {
19954 $addons = $this->get_addons();
19955 if ( empty( $addons ) ) {
19956 return array();
19957 }
19958
19959 $account_addons = $this->get_account_addons();
19960 if ( ! is_array( $account_addons ) ) {
19961 $account_addons = array();
19962 }
19963
19964 $user_licenses = $this->is_registered() ?
19965 $this->fetch_valid_user_licenses() :
19966 array();
19967
19968 if ( empty( $user_licenses ) ) {
19969 return $account_addons;
19970 }
19971
19972 $addon_ids = array();
19973 foreach ( $addons as $addon ) {
19974 $addon_ids[] = $addon->id;
19975 }
19976
19977 $license_product_ids = array();
19978
19979 foreach ( $user_licenses as $license ) {
19980 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
19981 $license_product_ids = array_merge( $license_product_ids, $license->products );
19982 } else {
19983 $license_product_ids[] = $license->plugin_id;
19984 }
19985 }
19986
19987 // Filter out non–add-on IDs.
19988 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
19989 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
19990 $this->_store_account_addons( array_unique( $new_account_addons ) );
19991 }
19992
19993 return $new_account_addons;
19994 }
19995
19996 /**
19997 * Store account params in the Database.
19998 *
19999 * @author Vova Feldman (@svovaf)
20000 * @since 1.0.1
20001 *
20002 * @param null|int $blog_id Since 2.0.0
20003 */
20004 private function _store_account( $blog_id = null ) {
20005 $this->_logger->entrance();
20006
20007 $this->_store_site( false, $blog_id );
20008 $this->_store_user( false );
20009 $this->_store_plans( false );
20010 $this->_store_licenses( false );
20011
20012 self::$_accounts->store( $blog_id );
20013 }
20014
20015 /**
20016 * Sync user's information.
20017 *
20018 * @author Vova Feldman (@svovaf)
20019 * @since 1.0.3
20020 * @uses FS_Api
20021 */
20022 private function _handle_account_user_sync() {
20023 $this->_logger->entrance();
20024
20025 $api = $this->get_api_user_scope();
20026
20027 // Get user's information.
20028 $user = $api->get( '/', true );
20029
20030 if ( isset( $user->id ) ) {
20031 $this->_user->first = $user->first;
20032 $this->_user->last = $user->last;
20033 $this->_user->email = $user->email;
20034
20035 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20036
20037 if ( $user->is_verified &&
20038 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20039 ) {
20040 $this->_user->is_verified = true;
20041
20042 $this->do_action( 'account_email_verified', $user->email );
20043
20044 $this->_admin_notices->add(
20045 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20046 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20047 'success',
20048 // Make admin sticky if account menu item is invisible,
20049 // since the page will be auto redirected to the plugin's
20050 // main settings page, and the non-sticky message
20051 // will disappear.
20052 ! $is_menu_item_account_visible,
20053 'email_verified'
20054 );
20055 }
20056
20057 // Flush user details to DB.
20058 $this->_store_user();
20059
20060 $this->do_action( 'after_account_user_sync', $user );
20061
20062 /**
20063 * If account menu item is hidden, redirect to plugin's main settings page.
20064 *
20065 * @author Vova Feldman (@svovaf)
20066 * @since 1.1.6
20067 *
20068 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20069 */
20070 if ( ! $is_menu_item_account_visible ) {
20071 fs_redirect( $this->_get_admin_page_url() );
20072 }
20073 }
20074 }
20075
20076 /**
20077 * @author Vova Feldman (@svovaf)
20078 * @since 1.0.9
20079 * @uses FS_Api
20080 *
20081 * @param number|bool $license_id
20082 *
20083 * @return FS_Subscription|object|bool
20084 */
20085 private function _fetch_site_license_subscription( $license_id = false ) {
20086 $this->_logger->entrance();
20087 $api = $this->get_api_site_scope();
20088
20089 if ( ! is_numeric( $license_id ) ) {
20090 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20091 $this->_license->parent_license_id :
20092 $this->_license->id;
20093 }
20094
20095 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20096
20097 return ! isset( $result->error ) ?
20098 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20099 new FS_Subscription( $result->subscriptions[0] ) :
20100 false
20101 ) :
20102 $result;
20103 }
20104
20105 /**
20106 * @author Vova Feldman (@svovaf)
20107 * @since 1.0.4
20108 * @uses FS_Api
20109 *
20110 * @param number|bool $plan_id
20111 *
20112 * @return FS_Plugin_Plan|object
20113 */
20114 private function _fetch_site_plan( $plan_id = false ) {
20115 $this->_logger->entrance();
20116 $api = $this->get_api_site_scope();
20117
20118 if ( ! is_numeric( $plan_id ) ) {
20119 $plan_id = $this->_site->plan_id;
20120 }
20121
20122 $plan = $api->get( "/plans/{$plan_id}.json", true );
20123
20124 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20125 }
20126
20127 /**
20128 * @author Vova Feldman (@svovaf)
20129 * @since 1.0.5
20130 * @uses FS_Api
20131 *
20132 * @return FS_Plugin_Plan[]|object
20133 */
20134 private function _fetch_plugin_plans() {
20135 $this->_logger->entrance();
20136 $api = $this->get_current_or_network_user_api_scope();
20137
20138 /**
20139 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20140 */
20141 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20142
20143 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20144 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20145 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20146 }
20147
20148 $result = $result->plans;
20149 }
20150
20151 return $result;
20152 }
20153
20154 /**
20155 * @author Vova Feldman (@svovaf)
20156 * @since 2.0.0
20157 *
20158 * @param number $plan_id
20159 *
20160 * @return \FS_Plugin_Plan|object
20161 */
20162 private function fetch_plan_by_id( $plan_id ) {
20163 $this->_logger->entrance();
20164 $api = $this->get_current_or_network_user_api_scope();
20165
20166 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20167
20168 return $this->is_api_result_entity( $result ) ?
20169 new FS_Plugin_Plan( $result ) :
20170 $result;
20171 }
20172
20173 /**
20174 * @author Vova Feldman (@svovaf)
20175 * @since 1.0.5
20176 * @uses FS_Api
20177 *
20178 * @param number|bool $plugin_id
20179 * @param number|bool $site_license_id
20180 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20181 * @param number|null $blog_id
20182 *
20183 * @return FS_Plugin_License[]|object
20184 */
20185 private function _fetch_licenses(
20186 $plugin_id = false,
20187 $site_license_id = false,
20188 $foreign_licenses = array(),
20189 $blog_id = null
20190 ) {
20191 $this->_logger->entrance();
20192
20193 $api = $this->get_api_user_scope();
20194
20195 if ( ! is_numeric( $plugin_id ) ) {
20196 $plugin_id = $this->_plugin->id;
20197 }
20198
20199 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20200 if ( ! empty ( $foreign_licenses ) ) {
20201 $foreign_licenses = array(
20202 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20203 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20204 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20205 );
20206
20207 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20208 }
20209
20210 $result = $api->get( $user_licenses_endpoint, true );
20211
20212 $is_site_license_synced = false;
20213
20214 $api_errors = array();
20215
20216 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20217 is_array( $result->licenses )
20218 ) {
20219 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20220 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20221
20222 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20223 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20224 }
20225 }
20226
20227 $result = $result->licenses;
20228 } else {
20229 $api_errors[] = $result;
20230 $result = array();
20231 }
20232
20233 if ( ! $is_site_license_synced ) {
20234 if ( ! is_null( $blog_id ) ) {
20235 /**
20236 * If blog ID is not null, the request is for syncing of the license of a single site via the
20237 * network-level "Account" page.
20238 *
20239 * @author Leo Fajardo (@leorw)
20240 */
20241 $this->switch_to_blog( $blog_id );
20242 }
20243
20244 $api = $this->get_api_site_scope();
20245
20246 if ( is_numeric( $site_license_id ) ) {
20247 // Try to retrieve a foreign license that is linked to the install.
20248 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20249
20250 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20251 is_array( $api_result->licenses )
20252 ) {
20253 $licenses = $api_result->licenses;
20254
20255 if ( ! empty( $licenses ) ) {
20256 $result[] = new FS_Plugin_License( $licenses[0] );
20257 }
20258 } else {
20259 $api_errors[] = $api_result;
20260 }
20261 } else if (
20262 is_object( $this->_license ) &&
20263 /**
20264 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20265 * the FS instance that does the syncing is the parent FS instance.
20266 *
20267 * @author Leo Fajardo (@leorw)
20268 * @since 2.3.0
20269 */
20270 $this->_license->plugin_id == $plugin_id
20271 ) {
20272 $is_license_in_result = false;
20273 if ( ! empty( $result ) ) {
20274 foreach ( $result as $license ) {
20275 if ( $license->id == $this->_license->id ) {
20276 $is_license_in_result = true;
20277 break;
20278 }
20279 }
20280 }
20281
20282 if ( ! $is_license_in_result ) {
20283 // Fetch foreign license by ID and license key.
20284 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20285 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20286
20287 if ( $this->is_api_result_entity( $license ) ) {
20288 $result[] = new FS_Plugin_License( $license );
20289 } else {
20290 $api_errors[] = $license;
20291 }
20292 }
20293 }
20294
20295 if ( ! is_null( $blog_id ) ) {
20296 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20297 }
20298 }
20299
20300 if ( is_array( $result ) && 0 < count( $result ) ) {
20301 // If found at least one license, return license collection even if there are errors.
20302 return $result;
20303 }
20304
20305 if ( ! empty( $api_errors ) ) {
20306 // If found any errors and no licenses, return first error.
20307 return $api_errors[0];
20308 }
20309
20310 // Fallback to empty licenses list.
20311 return $result;
20312 }
20313
20314 /**
20315 * @author Vova Feldman (@svovaf)
20316 * @since 2.0.0
20317 *
20318 * @param number $license_id
20319 * @param string $license_key
20320 *
20321 * @return \FS_Plugin_License|object
20322 */
20323 private function fetch_license_by_key( $license_id, $license_key ) {
20324 $this->_logger->entrance();
20325
20326 $api = $this->get_current_or_network_user_api_scope();
20327
20328 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20329
20330 return $this->is_api_result_entity( $result ) ?
20331 new FS_Plugin_License( $result ) :
20332 $result;
20333 }
20334
20335 /**
20336 * @author Vova Feldman (@svovaf)
20337 * @since 1.2.0
20338 * @uses FS_Api
20339 *
20340 * @param number|bool $plugin_id
20341 * @param bool $flush
20342 *
20343 * @return FS_Payment[]|object
20344 */
20345 function _fetch_payments( $plugin_id = false, $flush = false ) {
20346 $this->_logger->entrance();
20347
20348 $api = $this->get_api_user_scope();
20349
20350 if ( ! is_numeric( $plugin_id ) ) {
20351 $plugin_id = $this->_plugin->id;
20352 }
20353
20354 $include_bundles = (
20355 is_object( $this->_plugin ) &&
20356 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20357 );
20358
20359 $result = $api->get(
20360 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20361 $flush
20362 );
20363
20364 if ( ! isset( $result->error ) ) {
20365 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20366 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20367 }
20368 $result = $result->payments;
20369 }
20370
20371 return $result;
20372 }
20373
20374 /**
20375 * @author Vova Feldman (@svovaf)
20376 * @since 1.2.1.5
20377 * @uses FS_Api
20378 *
20379 * @param bool $flush
20380 *
20381 * @return \FS_Billing|mixed
20382 */
20383 function _fetch_billing( $flush = false ) {
20384 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20385
20386 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20387
20388 if ( $this->is_api_result_entity( $billing ) ) {
20389 $billing = new FS_Billing( $billing );
20390 }
20391
20392 return $billing;
20393 }
20394
20395 /**
20396 * @author Vova Feldman (@svovaf)
20397 * @since 1.0.5
20398 *
20399 * @param FS_Plugin_License[] $licenses
20400 * @param number $module_id
20401 */
20402 private function _update_licenses( $licenses, $module_id ) {
20403 $this->_logger->entrance();
20404
20405 if ( is_array( $licenses ) ) {
20406 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20407 $licenses[ $i ]->updated = time();
20408 }
20409 }
20410
20411 $this->_store_licenses( true, $module_id, $licenses );
20412 }
20413
20414 /**
20415 * @author Vova Feldman (@svovaf)
20416 * @since 1.0.4
20417 *
20418 * @param bool|number $plugin_id
20419 * @param bool $flush Since 1.1.7.3
20420 * @param int $expiration Since 1.2.2.7
20421 * @param bool|string $newer_than Since 2.2.1
20422 *
20423 * @return object|false New plugin tag info if exist.
20424 */
20425 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20426 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20427
20428 if ( ! is_object( $latest_tag ) ) {
20429 return false;
20430 }
20431
20432 $plugin_version = $this->get_plugin_version();
20433
20434 // Check if version is actually newer.
20435 $has_new_version =
20436 // If it's an non-installed add-on then always return latest.
20437 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20438 // Compare versions.
20439 version_compare( $plugin_version, $latest_tag->version, '<' );
20440
20441 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20442
20443 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20444
20445 $this->_storage->beta_data = array(
20446 'is_beta' => $is_latest_version_beta,
20447 'version' => $latest_tag->version
20448 );
20449
20450 return $has_new_version ? $latest_tag : false;
20451 }
20452
20453 /**
20454 * @author Vova Feldman (@svovaf)
20455 * @since 1.0.5
20456 *
20457 * @param bool|number $plugin_id
20458 * @param bool $flush Since 1.1.7.3
20459 * @param int $expiration Since 1.2.2.7
20460 * @param bool|string $newer_than Since 2.2.1
20461 *
20462 * @return bool|FS_Plugin_Tag
20463 */
20464 function get_update( $plugin_id = false, $flush = true, $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION, $newer_than = false ) {
20465 $this->_logger->entrance();
20466
20467 if ( ! is_numeric( $plugin_id ) ) {
20468 $plugin_id = $this->_plugin->id;
20469 }
20470
20471 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20472 $updates = $this->get_all_updates();
20473
20474 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20475 }
20476
20477 /**
20478 * Check if site assigned with active license.
20479 *
20480 * @author Vova Feldman (@svovaf)
20481 * @since 1.0.6
20482 *
20483 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20484 */
20485 function has_active_license() {
20486 return (
20487 is_object( $this->_license ) &&
20488 is_numeric( $this->_license->id ) &&
20489 ! $this->_license->is_expired()
20490 );
20491 }
20492
20493 /**
20494 * Check if site assigned with active & valid (not expired) license.
20495 *
20496 * @author Vova Feldman (@svovaf)
20497 * @since 1.2.1
20498 *
20499 * @param bool $check_expiration
20500 */
20501 function has_active_valid_license( $check_expiration = true ) {
20502 return self::is_active_valid_license( $this->_license, $check_expiration );
20503 }
20504
20505 /**
20506 * @author Leo Fajardo (@leorw)
20507 * @since 2.3.1
20508 */
20509 function is_data_debug_mode() {
20510 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20511 return false;
20512 }
20513
20514 $fs = $this->is_addon() ?
20515 $this->get_parent_instance() :
20516 $this;
20517
20518 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20519 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20520 } else {
20521 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20522 }
20523
20524 return ( 'true' === $is_developer_license_debug_mode );
20525 }
20526
20527 /**
20528 * @author Leo Fajardo (@leorw)
20529 * @since 2.3.1
20530 */
20531 function _set_data_debug_mode() {
20532 if ( ! $this->is_whitelabeled( true ) ) {
20533 return;
20534 }
20535
20536 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20537
20538 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20539 'true' :
20540 'false';
20541
20542 if ( 'true' === $transient_value ) {
20543 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20544 'last_license_key' :
20545 'last_license_user_key'
20546 );
20547
20548 if ( md5( $license_or_user_key ) !== $stored_key ) {
20549 $this->shoot_ajax_failure( sprintf(
20550 '%s... %s',
20551 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20552 $this->get_text_inline(
20553 'seems like the key you entered doesn\'t match our records.',
20554 'developer-or-license-not-found'
20555 )
20556 ) );
20557 }
20558 }
20559
20560 if ( $this->is_network_active() && fs_is_network_admin() ) {
20561 set_site_transient(
20562 "fs_{$this->get_id()}_data_debug_mode",
20563 $transient_value,
20564 WP_FS__TIME_24_HOURS_IN_SEC / 24
20565 );
20566 } else {
20567 set_transient(
20568 "fs_{$this->get_id()}_data_debug_mode",
20569 $transient_value,
20570 WP_FS__TIME_24_HOURS_IN_SEC / 24
20571 );
20572 }
20573
20574 if ( 'true' === $transient_value ) {
20575 $this->_admin_notices->add_sticky(
20576 $this->get_text_inline(
20577 '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.',
20578 'data_debug_mode_enabled'
20579 ),
20580 'data_debug_mode_enabled'
20581 );
20582 }
20583
20584 $this->shoot_ajax_success();
20585 }
20586
20587 /**
20588 * Check if a given license is active & valid (not expired).
20589 *
20590 * @author Vova Feldman (@svovaf)
20591 * @since 2.1.3
20592 *
20593 * @param FS_Plugin_License $license
20594 * @param bool $check_expiration
20595 *
20596 * @return bool
20597 */
20598 private static function is_active_valid_license( $license, $check_expiration = true ) {
20599 return (
20600 is_object( $license ) &&
20601 FS_Plugin_License::is_valid_id( $license->id ) &&
20602 $license->is_active() &&
20603 ( ! $check_expiration || $license->is_valid() )
20604 );
20605 }
20606
20607 /**
20608 * Checks if there's any site that is associated with an active & valid license.
20609 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20610 *
20611 * @author Vova Feldman (@svovaf)
20612 * @since 2.1.3
20613 *
20614 * @return bool
20615 */
20616 function has_any_active_valid_license() {
20617 if ( ! fs_is_network_admin() ) {
20618 return $this->has_active_valid_license();
20619 }
20620
20621 $installs = $this->get_blog_install_map();
20622 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20623
20624 foreach ( $installs as $blog_id => $install ) {
20625 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20626 continue;
20627 }
20628
20629 foreach ( $all_plugin_licenses as $license ) {
20630 if ( $license->id == $install->license_id ) {
20631 if ( self::is_active_valid_license( $license ) ) {
20632 return true;
20633 }
20634 }
20635 }
20636 }
20637
20638 return false;
20639 }
20640
20641 /**
20642 * Check if site assigned with license with enabled features.
20643 *
20644 * @author Vova Feldman (@svovaf)
20645 * @since 1.0.6
20646 *
20647 * @return bool
20648 */
20649 function has_features_enabled_license() {
20650 return (
20651 is_object( $this->_license ) &&
20652 is_numeric( $this->_license->id ) &&
20653 $this->_license->is_features_enabled()
20654 );
20655 }
20656
20657 /**
20658 * Checks if the product is activated with a bundle license.
20659 *
20660 * @author Leo Fajardo (@leorw)
20661 * @since 2.4.0
20662 *
20663 * @return bool
20664 */
20665 function is_activated_with_bundle_license() {
20666 if ( ! $this->has_features_enabled_license() ) {
20667 return false;
20668 }
20669
20670 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20671 }
20672
20673 /**
20674 * Check if user is a trial or have feature enabled license.
20675 *
20676 * @author Vova Feldman (@svovaf)
20677 * @since 1.1.7
20678 *
20679 * @return bool
20680 */
20681 function can_use_premium_code() {
20682 return $this->is_trial() || $this->has_features_enabled_license();
20683 }
20684
20685 /**
20686 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20687 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20688 * the context user is authenticated.
20689 *
20690 * @author Leo Fajardo (@leorw)
20691 * @since 1.2.2
20692 *
20693 * @return bool
20694 */
20695 function is_user_admin() {
20696 /**
20697 * Require a super-admin when network activated, running from the network level OR if
20698 * running from the site level but not delegated the opt-in.
20699 *
20700 * @author Vova Feldman (@svovaf)
20701 * @since 2.0.0
20702 */
20703 if ( $this->_is_network_active &&
20704 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20705 ) {
20706 return is_super_admin();
20707 }
20708
20709 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20710 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20711 }
20712
20713 /**
20714 * Sync site's plan.
20715 *
20716 * @author Vova Feldman (@svovaf)
20717 * @since 1.0.3
20718 *
20719 * @uses FS_Api
20720 *
20721 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20722 * the admin.
20723 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20724 * network-level "Account" page.
20725 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20726 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20727 * updates for a single site in case `execute_cron` has switched to a different blog.
20728 */
20729 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20730 $this->_logger->entrance();
20731
20732 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20733
20734 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20735
20736 if ( $is_addon_sync ) {
20737 $this->_sync_addon_license( $plugin_id, $background );
20738 } else {
20739 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20740 }
20741
20742 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20743 }
20744
20745 /**
20746 * Sync plugin's add-on license.
20747 *
20748 * @author Vova Feldman (@svovaf)
20749 * @since 1.0.6
20750 * @uses FS_Api
20751 *
20752 * @param number $addon_id
20753 * @param bool $background
20754 */
20755 private function _sync_addon_license( $addon_id, $background ) {
20756 $this->_logger->entrance();
20757
20758 if ( $this->is_addon_activated( $addon_id ) ) {
20759 // If already installed, use add-on sync.
20760 $fs_addon = self::get_instance_by_id( $addon_id );
20761
20762 if (
20763 // Add-on is network activated and network integrated.
20764 $fs_addon->is_network_active() ||
20765 // Background sync cron.
20766 self::is_cron() ||
20767 // Add-on is not network activated or not network integrated.
20768 ! fs_is_network_admin()
20769 ) {
20770 $fs_addon->_sync_license( $background );
20771
20772 return;
20773 }
20774 }
20775
20776 // Validate add-on exists.
20777 $addon = $this->get_addon( $addon_id );
20778
20779 if ( ! is_object( $addon ) ) {
20780 return;
20781 }
20782
20783 // Add add-on into account add-ons.
20784 $account_addons = $this->get_account_addons();
20785 if ( ! is_array( $account_addons ) ) {
20786 $account_addons = array();
20787 }
20788 $account_addons[] = $addon->id;
20789 $account_addons = array_unique( $account_addons );
20790 $this->_store_account_addons( $account_addons );
20791
20792 // Load add-on licenses.
20793 $licenses = $this->_fetch_licenses( $addon->id );
20794
20795 // Sync add-on licenses.
20796 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
20797 $this->_update_licenses( $licenses, $addon->id );
20798
20799 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
20800 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
20801
20802 if ( ! isset( $plans_result->error ) ) {
20803 $plans = array();
20804 foreach ( $plans_result->plans as $plan ) {
20805 $plans[] = new FS_Plugin_Plan( $plan );
20806 }
20807
20808 $this->_admin_notices->add_sticky(
20809 sprintf(
20810 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
20811 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
20812 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
20813 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
20814 $addon->title
20815 ) . ' ' . $this->get_latest_download_link(
20816 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
20817 $addon_id
20818 ),
20819 'addon_plan_upgraded_' . $addon->slug,
20820 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20821 );
20822 }
20823 }
20824 }
20825 }
20826
20827 /**
20828 * Sync site's plugin plan.
20829 *
20830 * @author Vova Feldman (@svovaf)
20831 * @since 1.0.6
20832 * @uses FS_Api
20833 *
20834 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
20835 * @param bool $send_installs_update Since 2.0.0
20836 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
20837 * syncing its license from the network-level "Account" page (e.g.: after
20838 * activating a license only for the single install).
20839 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
20840 * can be used here to switch to the previous blog in case `execute_cron`
20841 * has switched to a different blog.
20842 */
20843 private function _sync_plugin_license(
20844 $background = false,
20845 $send_installs_update = true,
20846 $is_context_single_site = false,
20847 $current_blog_id = null
20848 ) {
20849 $this->_logger->entrance();
20850
20851 $plan_change = 'none';
20852
20853 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
20854
20855 if ( ! $send_installs_update ) {
20856 $site = $this->_site;
20857 } else {
20858 /**
20859 * Sync site info.
20860 *
20861 * @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.
20862 */
20863 if ( $is_site_level_sync ) {
20864 /**
20865 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
20866 *
20867 * @author Leo Fajardo (@leorw)
20868 * @since 2.2.3
20869 */
20870 if ( is_numeric( $current_blog_id ) ) {
20871 $this->switch_to_blog( $current_blog_id );
20872 }
20873
20874 $result = $this->send_install_update( array(), true, true );
20875 $is_valid = $this->is_api_result_entity( $result );
20876 } else {
20877 $result = $this->send_installs_update( array(), true, true );
20878 $is_valid = $this->is_api_result_object( $result, 'installs' );
20879 }
20880
20881 if ( ! $is_valid ) {
20882 if ( $is_context_single_site ) {
20883 // Switch back to the main blog so that the following logic will have the right entities.
20884 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20885 }
20886
20887 // Show API message only if not background sync or if paying customer.
20888 if ( ! $background || $this->is_paying() ) {
20889 // Try to ping API to see if not blocked.
20890 if ( FS_Api::is_blocked( $result ) ) {
20891 /**
20892 * @author Vova Feldman (@svovaf)
20893 * @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.
20894 */
20895 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
20896 // Add notice immediately if not a background sync.
20897 $add_notice = ( ! $background );
20898
20899 if ( ! $add_notice ) {
20900 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
20901
20902 // We only want to add the notice after 3 consecutive failures.
20903 $add_notice = ( 3 <= $counter );
20904
20905 if ( ! $add_notice ) {
20906 /**
20907 * Update counter transient only if notice shouldn't be added. If it is added the transient will be reset anyway, because the retries mechanism should only start counting if the admin isn't aware of the connectivity issue.
20908 *
20909 * Also, since the background sync happens once a day, setting the transient expiration for a week should be enough to count 3 failures, if there's an actual connectivity issue.
20910 */
20911 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
20912 }
20913 }
20914
20915 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
20916 if ( $add_notice ) {
20917 self::$_global_admin_notices->add(
20918 $this->generate_api_blocked_notice_message_from_result( $result ),
20919 '',
20920 'error',
20921 $background,
20922 'api_blocked'
20923 );
20924
20925 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
20926
20927 // Notice was just shown, reset connectivity counter.
20928 delete_transient( '_fs_api_connection_retry_counter' );
20929 }
20930 }
20931 } else if ( is_object( $result ) ) {
20932 // Authentication params are broken.
20933 $this->_admin_notices->add(
20934 $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 ),
20935 '',
20936 'error'
20937 );
20938 }
20939 }
20940
20941 // No reason to continue with license sync while there are API issues.
20942 return;
20943 }
20944
20945 // API is working now. Delete the transient and start afresh.
20946 delete_transient('_fs_api_connection_retry_counter');
20947
20948 if ( $is_site_level_sync ) {
20949 $site = new FS_Site( $result );
20950 } else {
20951 // Map site addresses to their blog IDs.
20952 $address_to_blog_map = $this->get_address_to_blog_map();
20953
20954 // Find the current context install.
20955 $site = null;
20956 foreach ( $result->installs as $install ) {
20957 if ( $install->id == $this->_site->id ) {
20958 $site = new FS_Site( $install );
20959 } else {
20960 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
20961 $blog_id = $address_to_blog_map[ $address ];
20962
20963 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
20964 }
20965 }
20966 }
20967
20968 // Sync plans.
20969 $this->_sync_plans();
20970 }
20971
20972 // Remove sticky API connectivity message.
20973 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
20974
20975 if ( ! $this->has_paid_plan() ) {
20976 $this->_site = $site;
20977 $this->_store_site(
20978 true,
20979 $is_site_level_sync ?
20980 null :
20981 $this->get_network_install_blog_id()
20982 );
20983 } else {
20984 $context_blog_id = 0;
20985
20986 if ( $is_context_single_site ) {
20987 $context_blog_id = get_current_blog_id();
20988
20989 // Switch back to the main blog in order to properly sync the license.
20990 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20991 }
20992
20993 /**
20994 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
20995 * associated with that ID is not included in the user's licenses collection.
20996 * Save previous value to manage remote license renewals.
20997 */
20998 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
20999 $this->_sync_licenses(
21000 $site->license_id,
21001 ( $is_context_single_site ?
21002 $context_blog_id :
21003 null
21004 )
21005 );
21006
21007 if ( $is_context_single_site ) {
21008 $this->switch_to_blog( $context_blog_id );
21009 }
21010
21011 // Check if plan / license changed.
21012 if ( $site->plan_id != $this->_site->plan_id ||
21013 // Check if trial started.
21014 $site->trial_plan_id != $this->_site->trial_plan_id ||
21015 $site->trial_ends != $this->_site->trial_ends ||
21016 // Check if license changed.
21017 $site->license_id != $this->_site->license_id
21018 ) {
21019 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21020 // New trial started.
21021 $this->_site = $site;
21022 $plan_change = 'trial_started';
21023
21024 // For trial with subscription use-case.
21025 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21026
21027 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21028 $this->_site = $site;
21029 $this->_update_site_license( $new_license );
21030 $this->_store_licenses();
21031
21032 $this->_sync_site_subscription( $this->_license );
21033 }
21034 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21035 // Was in trial, but now trial expired and no license ID.
21036 // New trial started.
21037 $this->_site = $site;
21038 $plan_change = 'trial_expired';
21039 } else {
21040 $is_free = $this->is_free_plan();
21041
21042 // Make sure license exist and not expired.
21043 $new_license = is_null( $site->license_id ) ?
21044 null :
21045 $this->_get_license_by_id( $site->license_id );
21046
21047 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21048 // License cancelled.
21049 $this->_site = $site;
21050 $this->_update_site_license( $new_license );
21051 $this->_store_licenses();
21052
21053 $plan_change = 'cancelled';
21054 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21055 // The license is expired, so ignore upgrade method.
21056 $this->_site = $site;
21057 } else {
21058 // License changed.
21059 $this->_site = $site;
21060
21061 /**
21062 * IMPORTANT:
21063 * 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.
21064 *
21065 * @author Vova Feldman (@svovaf)
21066 * @since 2.0.0
21067 */
21068 $this->_update_site_license( $new_license );
21069
21070 if ( ! $is_context_single_site &&
21071 fs_is_network_admin() &&
21072 $this->_is_network_active &&
21073 $new_license->quota > 1 &&
21074 get_blog_count() > 1
21075 ) {
21076 // See if license can activated on all sites.
21077 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21078 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21079 // Open the license activation dialog box on the account page.
21080 add_action( 'admin_footer', array(
21081 &$this,
21082 '_open_license_activation_dialog_box'
21083 ) );
21084 }
21085 }
21086 }
21087
21088 $this->_store_licenses();
21089
21090 $plan_change = $is_free ?
21091 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21092 ( is_object( $new_license ) ?
21093 'changed' :
21094 'downgraded' );
21095 }
21096 }
21097
21098 // Store updated site info.
21099 $this->_store_site(
21100 true,
21101 $is_site_level_sync ?
21102 null :
21103 $this->get_network_install_blog_id()
21104 );
21105 } else {
21106 if ( ! is_object( $this->_license ) ) {
21107 $this->maybe_update_whitelabel_flag(
21108 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21109 $this->get_license_by_id( $site->license_id ) :
21110 null
21111 );
21112 } else {
21113 $this->maybe_update_whitelabel_flag( $this->_license );
21114
21115 if ( $this->_license->is_expired() ) {
21116 if ( ! $this->has_features_enabled_license() ) {
21117 $this->_deactivate_license();
21118 $plan_change = 'downgraded';
21119 } else {
21120 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21121
21122 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21123 /**
21124 * Show the expired license notice every 14 days.
21125 *
21126 * @author Leo Fajardo (@leorw)
21127 * @since 2.3.1
21128 */
21129 $plan_change = 'expired';
21130 }
21131 }
21132 } else if ( $was_license_expired_before_sync ) {
21133 /**
21134 * If license was expired but it is not anymore.
21135 *
21136 *
21137 * @author Daniele Alessandra (@danielealessandra)
21138 */
21139 $plan_change = 'extended';
21140 }
21141 }
21142
21143 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21144 $this->_sync_site_subscription( $this->_license );
21145 }
21146 }
21147
21148 if ( ! $this->is_addon() &&
21149 $this->_site->is_beta() !== $site->is_beta()
21150 ) {
21151 // Beta flag updated.
21152 $this->_site = $site;
21153
21154 $this->_store_site(
21155 true,
21156 $is_site_level_sync ?
21157 null :
21158 $this->get_network_install_blog_id()
21159 );
21160 }
21161
21162 if ( $this->is_addon() || $this->has_addons() ) {
21163 /**
21164 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21165 * an updated valid user licenses collection will be fetched from the server which is used to also
21166 * update the account add-ons (add-ons the user has licenses for).
21167 *
21168 * @author Leo Fajardo (@leorw)
21169 * @since 2.2.4
21170 */
21171 $this->purge_valid_user_licenses_cache();
21172 }
21173 }
21174
21175 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21176
21177 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21178 switch ( $plan_change ) {
21179 case 'none':
21180 if ( ! $background && is_admin() ) {
21181 $plan = $this->is_trial() ?
21182 $this->get_trial_plan() :
21183 $this->get_plan();
21184
21185 if ( $plan->is_free() ) {
21186 $this->_admin_notices->add(
21187 sprintf(
21188 $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' ),
21189 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21190 ) . ' ' . sprintf(
21191 '<a href="%s">%s</a>',
21192 $this->contact_url(
21193 'bug',
21194 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' ),
21195 strtoupper( $plan->name )
21196 )
21197 ),
21198 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21199 ),
21200 $hmm_text
21201 );
21202 }
21203 }
21204 break;
21205 case 'upgraded':
21206 case 'activated':
21207 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21208
21209 $this->_admin_notices->remove_sticky( array(
21210 'trial_started',
21211 'trial_promotion',
21212 'trial_expired',
21213 'activation_complete',
21214 'license_expired',
21215 ) );
21216 break;
21217 case 'extended':
21218 $this->_admin_notices->remove_sticky( array(
21219 'trial_expired',
21220 'license_expired',
21221 ) );
21222 break;
21223 case 'changed':
21224 $this->_admin_notices->add_sticky(
21225 sprintf(
21226 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21227 $this->get_plan_title()
21228 ),
21229 'plan_changed'
21230 );
21231
21232 $this->_admin_notices->remove_sticky( array(
21233 'trial_started',
21234 'trial_promotion',
21235 'trial_expired',
21236 'activation_complete',
21237 ) );
21238 break;
21239 case 'downgraded':
21240 $this->_admin_notices->add_sticky(
21241 ($this->has_free_plan() ?
21242 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 ) :
21243 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21244 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) ) ),
21245 'license_expired',
21246 $hmm_text
21247 );
21248 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21249 break;
21250 case 'cancelled':
21251 $this->_admin_notices->add(
21252 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21253 sprintf(
21254 '<a href="%s">%s</a>',
21255 $this->contact_url( 'bug' ),
21256 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21257 ),
21258 $hmm_text,
21259 'error'
21260 );
21261 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21262 break;
21263 case 'expired':
21264 $this->_admin_notices->add_sticky(
21265 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 ),
21266 'license_expired',
21267 $hmm_text
21268 );
21269
21270 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21271
21272 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21273 break;
21274 case 'trial_started':
21275 $this->add_complete_upgrade_instructions_notice(
21276 sprintf(
21277 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21278 '<i>' . $this->get_plugin_name() . '</i>'
21279 ),
21280 'trial_started',
21281 $this->get_trial_plan()->title
21282 );
21283
21284 $this->_admin_notices->remove_sticky( array(
21285 'trial_promotion',
21286 ) );
21287 break;
21288 case 'trial_expired':
21289 $this->_admin_notices->add_sticky(
21290 ($this->has_free_plan() ?
21291 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21292 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21293 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))),
21294 'trial_expired',
21295 $hmm_text
21296 );
21297 $this->_admin_notices->remove_sticky( array(
21298 'trial_started',
21299 'trial_promotion',
21300 'plan_upgraded',
21301 ) );
21302 break;
21303 }
21304 }
21305
21306 if ( 'none' !== $plan_change ) {
21307 if (
21308 ! is_object( $this->_license ) ||
21309 ! $this->_license->is_whitelabeled
21310 ) {
21311 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21312 }
21313
21314 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21315 }
21316 }
21317
21318 /**
21319 * @author Leo Fajardo (@leorw)
21320 * @since 2.5.4
21321 *
21322 * @param mixed $result
21323 *
21324 * @return string
21325 */
21326 private function generate_api_blocked_notice_message_from_result( $result ) {
21327 $api_domains = $this->apply_filters( 'api_domains', array(
21328 'api.freemius.com',
21329 'wp.freemius.com',
21330 ) );
21331
21332 $api_domains_list_items = '';
21333
21334 foreach( $api_domains as $api_domain ) {
21335 $api_domains_list_items .= "<li>{$api_domain}</li>";
21336 }
21337
21338 $error_message = sprintf(
21339 $this->get_text_inline( 'Your server is blocking the access to Freemius\' API, which is crucial for %1$s synchronization. Please contact your host to whitelist the following domains:%2$s', 'server-blocking-access' ),
21340 $this->get_plugin_name(),
21341 "<ol>{$api_domains_list_items}</ol><a href='#' class='fs-api-request-error-show-details-link'>" . $this->get_text_inline( 'Show error details', 'show-error-details' ) . " <span class='dashicons dashicons-arrow-down-alt2'></span></a>"
21342 );
21343
21344 $error_message =
21345 "<div>{$error_message}</div>" .
21346 '<div class="fs-api-request-error-details" style="display: none">' .
21347 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21348 $result->error->message .
21349 '</div>';
21350
21351 return $error_message;
21352 }
21353
21354 /**
21355 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21356 *
21357 * @author Vova Feldman (@svovaf)
21358 * @since 2.0.0
21359 */
21360 public function _open_license_activation_dialog_box() {
21361 $vars = array( 'license_id' => $this->_site->license_id );
21362 fs_require_once_template( 'js/open-license-activation.php', $vars );
21363 }
21364
21365 /**
21366 * @author Vova Feldman (@svovaf)
21367 * @since 1.0.5
21368 *
21369 * @param bool $background
21370 * @param FS_Plugin_License|null $premium_license
21371 */
21372 protected function _activate_license( $background = false, $premium_license = null ) {
21373 $this->_logger->entrance();
21374
21375 if ( is_null( $premium_license ) ) {
21376 $license_id = fs_request_get( 'license_id' );
21377
21378 if ( is_object( $this->_site ) &&
21379 FS_Plugin_License::is_valid_id( $license_id ) &&
21380 $license_id == $this->_site->license_id
21381 ) {
21382 // License is already activated.
21383 return;
21384 }
21385
21386 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21387 $this->_get_license_by_id( $license_id ) :
21388 $this->_get_available_premium_license();
21389 }
21390
21391 if ( ! is_object( $premium_license ) ) {
21392 return;
21393 }
21394
21395 if ( ! is_object( $this->_site ) ) {
21396 // Not yet opted-in.
21397 $user = $this->get_current_or_network_user();
21398 if ( ! is_object( $user ) ) {
21399 $user = self::_get_user_by_id( $premium_license->user_id );
21400 }
21401
21402 if ( is_object( $user ) ) {
21403 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21404 } else {
21405 $this->opt_in(
21406 false,
21407 false,
21408 false,
21409 $premium_license->secret_key
21410 );
21411
21412 return;
21413 }
21414 }
21415
21416
21417 /**
21418 * If the premium license is already associated with the install, just
21419 * update the license reference (activation is not required).
21420 *
21421 * @since 1.1.9
21422 */
21423 if ( $premium_license->id == $this->_site->license_id ) {
21424 // License is already activated.
21425 $this->_update_site_license( $premium_license );
21426 $this->_store_account();
21427
21428 return;
21429 }
21430
21431 if ( $this->_site->user_id != $premium_license->user_id ) {
21432 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21433 } else {
21434 $api_request_params = array();
21435 }
21436
21437 $api = $this->get_api_site_scope();
21438 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21439
21440 if ( ! $this->is_api_result_entity( $license ) ) {
21441 if ( ! $background ) {
21442 $this->_admin_notices->add( sprintf(
21443 '%s %s',
21444 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21445 ( is_object( $license ) && isset( $license->error ) ?
21446 $license->error->message :
21447 sprintf( '%s<br><code>%s</code>',
21448 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21449 var_export( $license, true )
21450 )
21451 )
21452 ),
21453 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21454 'error'
21455 );
21456 }
21457
21458 return;
21459 }
21460
21461 $premium_license = new FS_Plugin_License( $license );
21462
21463 // Updated site plan.
21464 $site = $this->get_api_site_scope()->get( '/', true );
21465 if ( $this->is_api_result_entity( $site ) ) {
21466 $this->_site = new FS_Site( $site );
21467 }
21468 $this->_update_site_license( $premium_license );
21469
21470 $this->_store_account();
21471
21472 if ( $this->is_addon() || $this->has_addons() ) {
21473 /**
21474 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21475 * an updated valid user licenses collection will be fetched from the server which is used to also
21476 * update the account add-ons (add-ons the user has licenses for).
21477 *
21478 * @author Leo Fajardo (@leorw)
21479 * @since 2.2.4
21480 */
21481 $this->purge_valid_user_licenses_cache();
21482 }
21483
21484 if ( ! $background ) {
21485 $this->add_complete_upgrade_instructions_notice(
21486 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21487 'license_activated'
21488 );
21489 }
21490
21491 $this->_admin_notices->remove_sticky( array(
21492 'trial_promotion',
21493 'license_expired',
21494 ) );
21495 }
21496
21497 /**
21498 * @author Vova Feldman (@svovaf)
21499 * @since 1.0.5
21500 *
21501 * @param bool $show_notice
21502 */
21503 protected function _deactivate_license( $show_notice = true ) {
21504 $this->_logger->entrance();
21505
21506 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21507
21508 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21509 $this->_admin_notices->add(
21510 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() ),
21511 $hmm_text
21512 );
21513
21514 return;
21515 }
21516
21517 $api = $this->get_api_site_scope();
21518 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21519
21520 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21521 }
21522
21523 /**
21524 * @author Leo Fajardo (@leorw)
21525 * @since 2.2.1
21526 *
21527 * @param FS_Plugin_License $license
21528 * @param bool|string $hmm_text
21529 * @param bool $show_notice
21530 */
21531 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21532 if ( isset( $license->error ) ) {
21533 $this->_admin_notices->add(
21534 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21535 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21536 $hmm_text,
21537 'error'
21538 );
21539
21540 return;
21541 }
21542
21543 // Update license cache.
21544 if ( is_array( $this->_licenses ) ) {
21545 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21546 if ( $license->id == $this->_licenses[ $i ]->id ) {
21547 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21548 }
21549 }
21550 }
21551
21552 // Update site plan to default.
21553 $this->_sync_plans();
21554 $this->_site->plan_id = $this->_plans[0]->id;
21555 // Unlink license from site.
21556 $this->_update_site_license( null );
21557
21558 $this->_store_account();
21559
21560 if ( $show_notice ) {
21561 $this->_admin_notices->add(
21562 sprintf( $this->is_only_premium() ?
21563 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21564 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21565 $this->get_plan_title()
21566 ),
21567 $this->get_text_inline( 'O.K', 'ok' )
21568 );
21569 }
21570
21571 $this->_admin_notices->remove_sticky( array(
21572 'plan_upgraded',
21573 'license_activated',
21574 ) );
21575 }
21576
21577 /**
21578 * Site plan downgrade.
21579 *
21580 * @author Vova Feldman (@svovaf)
21581 * @since 1.0.4
21582 *
21583 * @return object
21584 *
21585 * @uses FS_Api
21586 */
21587 private function _downgrade_site() {
21588 $this->_logger->entrance();
21589
21590 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21591
21592 $api = $this->get_api_site_scope();
21593 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21594
21595 $plan_downgraded = false;
21596 $plan = false;
21597 if ( $this->is_api_result_entity( $site ) ) {
21598 $prev_plan_id = $this->_site->plan_id;
21599
21600 // Update new site plan id.
21601 $this->_site->plan_id = $site->plan_id;
21602
21603 $plan = $this->get_plan();
21604 $subscription = $this->_sync_site_subscription( $this->_license );
21605
21606 // Plan downgraded if plan was changed or subscription was cancelled.
21607 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21608 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21609 } else {
21610 // handle different error cases.
21611 $this->handle_license_deactivation_result(
21612 $site,
21613 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21614 );
21615 }
21616
21617 if ( ! $plan_downgraded ) {
21618 return (object) array(
21619 'error' => (object) array(
21620 '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' )
21621 )
21622 );
21623 }
21624
21625 // Remove previous sticky message about upgrade (if exist).
21626 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21627
21628 $this->_admin_notices->add(
21629 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21630 $plan->title,
21631 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21632 )
21633 );
21634
21635 // Store site updates.
21636 $this->_store_site();
21637
21638 if ( $deactivate_license &&
21639 ! FS_Plugin_License::is_valid_id( $site->license_id )
21640 ) {
21641 if ( $this->_site->is_localhost() ) {
21642 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21643 } else {
21644 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21645 }
21646
21647 // Handle successful license deactivation result.
21648 $this->handle_license_deactivation_result( $this->_license );
21649 }
21650
21651 return $site;
21652 }
21653
21654 /**
21655 * @author Vova Feldman (@svovaf)
21656 * @since 1.1.8.1
21657 *
21658 * @param bool|string $plan_name
21659 * @param bool $add_sticky_notice
21660 *
21661 * @return bool If trial was successfully started.
21662 */
21663 function start_trial( $plan_name = false, $add_sticky_notice = false ) {
21664 $this->_logger->entrance();
21665
21666 // Alias.
21667 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21668
21669 if ( $this->is_trial() ) {
21670 // Already in trial mode.
21671 $this->_admin_notices->add(
21672 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21673 $oops_text,
21674 'error',
21675 $add_sticky_notice
21676 );
21677
21678 return false;
21679 }
21680
21681 if ( $this->_site->is_trial_utilized() && ! $this->is_payments_sandbox() ) {
21682 // Trial was already utilized.
21683 $this->_admin_notices->add(
21684 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21685 $oops_text,
21686 'error',
21687 $add_sticky_notice
21688 );
21689
21690 return false;
21691 }
21692
21693 if ( false !== $plan_name ) {
21694 $plan = $this->get_plan_by_name( $plan_name );
21695
21696 if ( false === $plan ) {
21697 // Plan doesn't exist.
21698 $this->_admin_notices->add(
21699 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21700 $oops_text,
21701 'error',
21702 $add_sticky_notice
21703 );
21704
21705 return false;
21706 }
21707
21708 if ( ! $plan->has_trial() ) {
21709 // Plan doesn't exist.
21710 $this->_admin_notices->add(
21711 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21712 $oops_text,
21713 'error',
21714 $add_sticky_notice
21715 );
21716
21717 return false;
21718 }
21719 } else {
21720 if ( ! $this->has_trial_plan() ) {
21721 // None of the plans have a trial.
21722 $this->_admin_notices->add(
21723 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21724 $oops_text,
21725 'error',
21726 $add_sticky_notice
21727 );
21728
21729 return false;
21730 }
21731
21732 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21733
21734 $plan = $plans_with_trial[0];
21735 }
21736
21737 $trial_params = array();
21738
21739 if ( $this->is_payments_sandbox() ) {
21740 $trial_params['trial_timestamp'] = time();
21741 $trial_params['trial_token'] = FS_Security::instance()->get_trial_token(
21742 $this->get_plugin(),
21743 $plan,
21744 $trial_params['trial_timestamp']
21745 );
21746 }
21747
21748 $api = $this->get_api_site_scope();
21749 $trial = $api->call( "plans/{$plan->id}/trials.json", 'post', $trial_params );
21750
21751 if ( ! $this->is_api_result_entity( $trial ) ) {
21752 // Some API error while trying to start the trial.
21753 $this->_admin_notices->add(
21754 $this->get_api_error_message( $trial ),
21755 $oops_text,
21756 'error',
21757 $add_sticky_notice
21758 );
21759
21760 return false;
21761 }
21762
21763 // Sync license.
21764 $this->_sync_license();
21765
21766 return $this->is_trial();
21767 }
21768
21769 /**
21770 * Cancel site trial.
21771 *
21772 * @author Vova Feldman (@svovaf)
21773 * @since 1.0.9
21774 *
21775 * @return object
21776 *
21777 * @uses FS_Api
21778 */
21779 private function _cancel_trial() {
21780 $this->_logger->entrance();
21781
21782 if ( ! $this->is_trial() ) {
21783 return (object) array(
21784 'error' => (object) array(
21785 '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' )
21786 )
21787 );
21788 }
21789
21790 $trial_plan = $this->get_trial_plan();
21791
21792 $api = $this->get_api_site_scope();
21793 $site = $api->call( 'trials.json', 'delete' );
21794
21795 $trial_cancelled = false;
21796
21797 if ( $this->is_api_result_entity( $site ) ) {
21798 $prev_trial_ends = $this->_site->trial_ends;
21799
21800 if ( $this->is_paid_trial() ) {
21801 $this->_license->expiration = $site->trial_ends;
21802 $this->_license->is_cancelled = true;
21803 $this->_update_site_license( $this->_license );
21804 $this->_store_licenses();
21805
21806 // Clear subscription reference.
21807 $this->_sync_site_subscription( null );
21808 }
21809
21810 // Update site info.
21811 $this->_site = new FS_Site( $site );
21812
21813 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
21814 } else {
21815 // @todo handle different error cases.
21816 }
21817
21818 if ( ! $trial_cancelled ) {
21819 return (object) array(
21820 'error' => (object) array(
21821 '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' )
21822 )
21823 );
21824 }
21825
21826 // Remove previous sticky messages about upgrade or trial (if exist).
21827 $this->_admin_notices->remove_sticky( array(
21828 'trial_started',
21829 'trial_promotion',
21830 'plan_upgraded',
21831 ) );
21832
21833 // Store site updates.
21834 $this->_store_site();
21835
21836 if ( ! $this->is_addon() ||
21837 ! $this->deactivate_premium_only_addon_without_license( true )
21838 ) {
21839 $this->_admin_notices->add(
21840 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
21841 );
21842 }
21843
21844 return $site;
21845 }
21846
21847 /**
21848 * @author Vova Feldman (@svovaf)
21849 * @since 1.0.6
21850 *
21851 * @param bool|number $plugin_id
21852 *
21853 * @return bool
21854 */
21855 private function _is_addon_id( $plugin_id ) {
21856 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
21857 }
21858
21859 /**
21860 * Check if user eligible to download premium version updates.
21861 *
21862 * @author Vova Feldman (@svovaf)
21863 * @since 1.0.6
21864 *
21865 * @return bool
21866 */
21867 private function _can_download_premium() {
21868 return $this->has_any_active_valid_license() ||
21869 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
21870 }
21871
21872 /**
21873 *
21874 * @author Vova Feldman (@svovaf)
21875 * @since 1.0.6
21876 *
21877 * @param bool|number $addon_id
21878 * @param string $type "json" or "zip"
21879 *
21880 * @return string
21881 */
21882 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
21883
21884 $is_addon = $this->_is_addon_id( $addon_id );
21885
21886 $is_premium = null;
21887 if ( ! $is_addon ) {
21888 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
21889 } else if ( $this->is_addon_activated( $addon_id ) ) {
21890 $fs_addon = self::get_instance_by_id( $addon_id );
21891 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
21892 }
21893
21894 // If add-on, then append add-on ID.
21895 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
21896 '/updates/latest.' . $type;
21897
21898 // If add-on and not yet activated, try to fetch based on server licensing.
21899 if ( is_bool( $is_premium ) ) {
21900 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
21901 }
21902
21903 if ( $this->has_secret_key() ) {
21904 $endpoint = add_query_arg( 'type', 'all', $endpoint );
21905 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
21906 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
21907 }
21908
21909 return $endpoint;
21910 }
21911
21912 /**
21913 * @author Vova Feldman (@svovaf)
21914 * @since 1.0.4
21915 *
21916 * @param bool|number $addon_id
21917 * @param bool $flush Since 1.1.7.3
21918 * @param int $expiration Since 1.2.2.7
21919 * @param bool|string $newer_than Since 2.2.1
21920 * @param bool|string $fetch_readme Since 2.2.1
21921 *
21922 * @return object|false Plugin latest tag info.
21923 */
21924 function _fetch_latest_version(
21925 $addon_id = false,
21926 $flush = true,
21927 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
21928 $newer_than = false,
21929 $fetch_readme = true
21930 ) {
21931 $this->_logger->entrance();
21932
21933 if ( $this->is_unresolved_clone( true ) ) {
21934 return false;
21935 }
21936
21937 $switch_to_blog_id = null;
21938
21939 /**
21940 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
21941 * @since 1.1.7.4 Also check updates for add-ons.
21942 */
21943 if (
21944 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
21945 ! $this->_is_addon_id( $addon_id )
21946 ) {
21947 if ( ! is_multisite() ) {
21948 return false;
21949 }
21950
21951 $installs_map = $this->get_blog_install_map();
21952
21953 foreach ( $installs_map as $blog_id => $install ) {
21954 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
21955 continue;
21956 }
21957
21958 /**
21959 * @var FS_Site $install
21960 */
21961 if ( $install->is_trial() ) {
21962 $switch_to_blog_id = $blog_id;
21963 break;
21964 }
21965
21966 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
21967 $license = $this->get_license_by_id( $install->license_id );
21968
21969 if ( is_object( $license ) && $license->is_features_enabled() ) {
21970 $switch_to_blog_id = $blog_id;
21971 break;
21972 }
21973 }
21974 }
21975
21976 if ( is_null( $switch_to_blog_id ) ) {
21977 return false;
21978 }
21979 }
21980
21981 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
21982 get_current_blog_id() :
21983 0;
21984
21985 if ( is_numeric( $switch_to_blog_id ) ) {
21986 $this->switch_to_blog( $switch_to_blog_id );
21987 }
21988
21989 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
21990
21991 if ( ! empty( $newer_than ) ) {
21992 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
21993 }
21994
21995 if ( true === $fetch_readme ) {
21996 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
21997
21998 // Don't cache the API response when fetching readme information.
21999 $expiration = null;
22000 }
22001
22002 $tag = $this->get_api_site_or_plugin_scope()->get(
22003 $latest_version_endpoint,
22004 $flush,
22005 $expiration
22006 );
22007
22008 if ( is_numeric( $switch_to_blog_id ) ) {
22009 $this->switch_to_blog( $current_blog_id );
22010 }
22011
22012 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22013
22014 $this->_logger->departure( 'Latest version ' . $latest_version );
22015
22016 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22017 }
22018
22019 #----------------------------------------------------------------------------------
22020 #region Download Plugin
22021 #----------------------------------------------------------------------------------
22022
22023 /**
22024 * Download latest plugin version, based on plan.
22025 *
22026 * Not like _download_latest(), this will redirect the page
22027 * to secure download url to prevent dual download (from FS to WP server,
22028 * and then from WP server to the client / browser).
22029 *
22030 * @author Vova Feldman (@svovaf)
22031 * @since 1.0.9
22032 *
22033 * @param bool|number $plugin_id
22034 *
22035 * @uses FS_Api
22036 * @uses wp_redirect()
22037 */
22038 private function download_latest_directly( $plugin_id = false ) {
22039 $this->_logger->entrance();
22040
22041 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22042 }
22043
22044 /**
22045 * Get latest plugin FS API download URL.
22046 *
22047 * @author Vova Feldman (@svovaf)
22048 * @since 1.0.9
22049 *
22050 * @param bool|number $plugin_id
22051 *
22052 * @return string
22053 */
22054 private function get_latest_download_api_url( $plugin_id = false ) {
22055 $this->_logger->entrance();
22056
22057 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22058 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22059 );
22060
22061 return str_replace( 'http:', 'https:', $download_api_url );
22062 }
22063
22064 /**
22065 * Get payment invoice URL.
22066 *
22067 * @author Vova Feldman (@svovaf)
22068 * @since 1.2.0
22069 *
22070 * @param bool|number $payment_id
22071 *
22072 * @return string
22073 */
22074 function _get_invoice_api_url( $payment_id = false ) {
22075 $this->_logger->entrance();
22076
22077 $url = $this->get_api_user_scope()->get_signed_url(
22078 "/payments/{$payment_id}/invoice.pdf"
22079 );
22080
22081 if ( ! fs_starts_with( $url, 'https://' ) ) {
22082 // Always use HTTPS for invoices.
22083 $url = 'https' . substr( $url, 4 );
22084 }
22085
22086 return $url;
22087 }
22088
22089 /**
22090 * Get latest plugin download link.
22091 *
22092 * @author Vova Feldman (@svovaf)
22093 * @since 1.0.9
22094 *
22095 * @param string $label
22096 * @param bool|number $plugin_id
22097 *
22098 * @return string
22099 */
22100 private function get_latest_download_link( $label, $plugin_id = false ) {
22101 return sprintf(
22102 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22103 $this->_get_latest_download_local_url( $plugin_id ),
22104 $label
22105 );
22106 }
22107
22108 /**
22109 * Get latest plugin download local URL.
22110 *
22111 * @author Vova Feldman (@svovaf)
22112 * @since 1.0.9
22113 *
22114 * @param bool|number $plugin_id
22115 *
22116 * @return string
22117 */
22118 function _get_latest_download_local_url( $plugin_id = false ) {
22119 // Add timestamp to protect from caching.
22120 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22121
22122 if ( ! empty( $plugin_id ) ) {
22123 $params['plugin_id'] = $plugin_id;
22124 } else if ( $this->is_addon() ) {
22125 $params['plugin_id'] = $this->get_id();
22126 }
22127
22128 $fs = $this->is_addon() ?
22129 $this->get_parent_instance() :
22130 $this;
22131
22132 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22133 }
22134
22135 #endregion Download Plugin ------------------------------------------------------------------
22136
22137 /**
22138 * @author Vova Feldman (@svovaf)
22139 * @since 1.0.4
22140 *
22141 * @uses FS_Api
22142 *
22143 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22144 * was initiated by the admin.
22145 * @param bool|number $plugin_id
22146 * @param bool $flush Since 1.1.7.3
22147 * @param int $expiration Since 1.2.2.7
22148 * @param bool|string $newer_than Since 2.2.1
22149 */
22150 private function check_updates(
22151 $background = false,
22152 $plugin_id = false,
22153 $flush = true,
22154 $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22155 $newer_than = false
22156 ) {
22157 $this->_logger->entrance();
22158
22159 // Check if there's a newer version for download.
22160 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22161
22162 $update = null;
22163 if ( is_object( $new_version ) ) {
22164 $update = new FS_Plugin_Tag( $new_version );
22165
22166 if ( ! $background ) {
22167 $this->_admin_notices->add(
22168 sprintf(
22169 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22170 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22171 $update->version,
22172 sprintf(
22173 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22174 $this->get_account_url( 'download_latest' ),
22175 sprintf(
22176 /* translators: %s: plan name (e.g. latest "Professional" version) */
22177 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22178 $this->get_plan_title()
22179 )
22180 )
22181 ),
22182 $this->get_text_inline( 'New', 'new' ) . '!'
22183 );
22184 }
22185 } else if ( false === $new_version && ! $background ) {
22186 $this->_admin_notices->add(
22187 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22188 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22189 );
22190 }
22191
22192 $this->_store_update( $update, true, $plugin_id );
22193 }
22194
22195 /**
22196 * @author Vova Feldman (@svovaf)
22197 * @since 1.0.4
22198 *
22199 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22200 *
22201 * @return FS_Plugin[]
22202 *
22203 * @uses FS_Api
22204 */
22205 private function sync_addons( $flush = false ) {
22206 $this->_logger->entrance();
22207
22208 $api = $this->get_api_site_or_plugin_scope();
22209
22210 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22211
22212 /**
22213 * @since 1.2.1
22214 *
22215 * If there's a cached version of the add-ons and not asking
22216 * for a flush, just use the currently stored add-ons.
22217 */
22218 if ( ! $flush && $api->is_cached( $path ) ) {
22219 $addons = self::get_all_addons();
22220
22221 return isset( $addons[ $this->_plugin->id ] ) ?
22222 $addons[ $this->_plugin->id ] :
22223 array();
22224 }
22225
22226 $result = $api->get( $path, $flush );
22227
22228 $addons = array();
22229 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22230 is_array( $result->plugins )
22231 ) {
22232 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22233 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22234 }
22235
22236 $this->_store_addons( $addons, true );
22237 }
22238
22239 return $addons;
22240 }
22241
22242 /**
22243 * Handle user email update.
22244 *
22245 * @author Vova Feldman (@svovaf)
22246 * @since 1.0.3
22247 * @uses FS_Api
22248 *
22249 * @param string $new_email
22250 *
22251 * @return object
22252 */
22253 private function update_email( $new_email ) {
22254 $this->_logger->entrance();
22255
22256 $api = $this->get_api_user_scope();
22257 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22258 'email' => $new_email,
22259 'after_email_confirm_url' => $this->_get_admin_page_url(
22260 'account',
22261 array( 'fs_action' => 'sync_user' )
22262 ),
22263 ) );
22264
22265 if ( ! isset( $user->error ) ) {
22266 $this->_user->email = $user->email;
22267 $this->_user->is_verified = $user->is_verified;
22268 $this->_store_user();
22269 } else {
22270 // handle different error cases.
22271 }
22272
22273 return $user;
22274 }
22275
22276 #----------------------------------------------------------------------------------
22277 #region API Error Handling
22278 #----------------------------------------------------------------------------------
22279
22280 /**
22281 * @author Vova Feldman (@svovaf)
22282 * @since 1.1.1
22283 *
22284 * @param mixed $result
22285 *
22286 * @return bool Is API result contains an error.
22287 */
22288 private function is_api_error( $result ) {
22289 return FS_Api::is_api_error( $result );
22290 }
22291
22292 /**
22293 * Checks if given API result is a non-empty and not an error object.
22294 *
22295 * @author Vova Feldman (@svovaf)
22296 * @since 1.2.1.5
22297 *
22298 * @param mixed $result
22299 * @param string|null $required_property Optional property we want to verify that is set.
22300 *
22301 * @return bool
22302 */
22303 function is_api_result_object( $result, $required_property = null ) {
22304 return FS_Api::is_api_result_object( $result, $required_property );
22305 }
22306
22307 /**
22308 * Checks if given API result is a non-empty entity object with non-empty ID.
22309 *
22310 * @author Vova Feldman (@svovaf)
22311 * @since 1.2.1.5
22312 *
22313 * @param mixed $result
22314 *
22315 * @return bool
22316 */
22317 private function is_api_result_entity( $result ) {
22318 return FS_Api::is_api_result_entity( $result );
22319 }
22320
22321 #endregion
22322
22323 /**
22324 * Make sure a given argument is an array of a specific type.
22325 *
22326 * @author Vova Feldman (@svovaf)
22327 * @since 1.2.1.5
22328 *
22329 * @param mixed $array
22330 * @param string $class
22331 *
22332 * @return bool
22333 */
22334 private function is_array_instanceof( $array, $class ) {
22335 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22336 }
22337
22338 /**
22339 * Start install ownership change.
22340 *
22341 * @author Vova Feldman (@svovaf)
22342 * @since 1.1.1
22343 * @uses FS_Api
22344 *
22345 * @param string $candidate_email
22346 * @param string $transfer_type
22347 *
22348 * @return bool Is ownership change successfully initiated.
22349 */
22350 private function init_change_owner( $candidate_email, $transfer_type ) {
22351 $this->_logger->entrance();
22352
22353 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22354 $install_ids = array();
22355
22356 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22357 $install = $install_info['install'];
22358
22359 if ( $this->_user->id != $install->user_id ) {
22360 // Skip add-on installs that are not owned by the parent product's install's owner.
22361 continue;
22362 }
22363
22364 $install_ids[ $slug ] = $install->id;
22365 }
22366
22367 $api = $this->get_api_site_scope();
22368 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22369 'email' => $candidate_email,
22370 'transfer_type' => $transfer_type,
22371 'install_ids' => implode( ',', array_values( $install_ids ) ),
22372 'after_confirm_url' => $this->_get_admin_page_url(
22373 'account',
22374 array( 'fs_action' => 'change_owner' )
22375 ),
22376 ) );
22377
22378 return ! $this->is_api_error( $result );
22379 }
22380
22381 /**
22382 * Handle install ownership change.
22383 *
22384 * @author Vova Feldman (@svovaf)
22385 * @since 1.1.1
22386 * @uses FS_Api
22387 *
22388 * @return bool Was ownership change successfully complete.
22389 */
22390 private function complete_change_owner() {
22391 $this->_logger->entrance();
22392
22393 $install_ids = fs_request_get( 'install_ids' );
22394
22395 if ( ! empty( $install_ids ) ) {
22396 $install_ids = explode( ',', $install_ids );
22397
22398 foreach ( $install_ids as $key => $install_id ) {
22399 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22400 unset( $install_ids[ $key ] );
22401 }
22402 }
22403 }
22404
22405 if ( ! is_array( $install_ids ) ) {
22406 $install_ids = array();
22407 }
22408
22409 $user = new FS_User();
22410 $user->id = fs_request_get( 'user_id' );
22411 $user->public_key = fs_request_get_raw( 'user_public_key' );
22412 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22413
22414 $prev_user = $this->_user;
22415 $this->_user = $user;
22416
22417 $result = $this->get_api_user_scope( true )->get(
22418 "/installs.json?install_ids=" . implode( ',', $install_ids )
22419 );
22420
22421 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22422
22423 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22424 $site_id_slug_map = array();
22425
22426 foreach ( $current_blog_sites as $slug => $site ) {
22427 $site_id_slug_map[ $site->id ] = $slug;
22428 }
22429
22430 foreach ( $result->installs as $install ) {
22431 $site = new FS_Site( $install );
22432
22433 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22434 continue;
22435 }
22436
22437 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22438
22439 if ( $this->_site->id == $site->id ) {
22440 $this->_site = $site;
22441 }
22442 }
22443 }
22444
22445 // Validate install's user and given user.
22446 if ( $user->id != $this->_site->user_id ) {
22447 $this->_user = $prev_user;
22448
22449 return false;
22450 }
22451
22452 $this->set_account_option( 'sites', $current_blog_sites, true );
22453
22454 // Fetch new user information.
22455 $user_result = $this->get_api_user_scope( true )->get();
22456 $user = new FS_User( $user_result );
22457 $this->_user = $user;
22458
22459 $this->_set_account( $user, $this->_site );
22460
22461 $remove_user = true;
22462 $all_modules_sites = FS_DebugManager::get_all_modules_sites();
22463
22464 foreach ( $all_modules_sites as $sites_by_module_type ) {
22465 foreach ( $sites_by_module_type as $sites_by_slug ) {
22466 foreach ( $sites_by_slug as $site ) {
22467 if ( $prev_user->id == $site->user_id ) {
22468 $remove_user = false;
22469 break;
22470 }
22471 }
22472
22473 if ( ! $remove_user ) {
22474 break;
22475 }
22476 }
22477
22478 if ( ! $remove_user ) {
22479 break;
22480 }
22481 }
22482
22483 if ( $remove_user ) {
22484 $users = self::get_all_users();
22485
22486 if ( isset( $users[ $prev_user->id ] ) ) {
22487 unset( $users[ $prev_user->id ] );
22488 } else {
22489 // If the prev user wasn't found by the key, iterate over the users collection.
22490 foreach ( $users as $key => $user ) {
22491 if ( $user->id == $prev_user->id ) {
22492 unset( $users[ $key ] );
22493 break;
22494 }
22495 }
22496 }
22497
22498 $this->set_account_option( 'users', $users, true );
22499 }
22500
22501 return true;
22502 }
22503
22504 /**
22505 * Completes ownership change by license.
22506 *
22507 * @author Leo Fajardo (@leorw)
22508 * @since 2.3.2
22509 *
22510 * @param number $user_id
22511 * @param array[string]number $install_ids_by_slug_map
22512 *
22513 */
22514 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22515 $this->_logger->entrance();
22516
22517 $this->sync_user_by_current_install( $user_id );
22518
22519 $result = $this->get_api_user_scope( true )->get(
22520 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22521 );
22522
22523 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22524 $sites = self::get_all_sites( $this->get_module_type() );
22525 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22526
22527 foreach ( $result->installs as $install ) {
22528 $site = new FS_Site( $install );
22529
22530 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22531 }
22532
22533 $this->set_account_option( 'sites', $sites, true );
22534 }
22535 }
22536
22537 /**
22538 * Handle user name update.
22539 *
22540 * @author Vova Feldman (@svovaf)
22541 * @since 1.0.9
22542 * @uses FS_Api
22543 *
22544 * @return object
22545 */
22546 private function update_user_name() {
22547 $this->_logger->entrance();
22548 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22549
22550 $api = $this->get_api_user_scope();
22551 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22552 'name' => $name,
22553 ) );
22554
22555 if ( ! isset( $user->error ) ) {
22556 $this->_user->first = $user->first;
22557 $this->_user->last = $user->last;
22558 $this->_store_user();
22559 } else {
22560 // handle different error cases.
22561
22562 }
22563
22564 return $user;
22565 }
22566
22567 /**
22568 * Verify user email.
22569 *
22570 * @author Vova Feldman (@svovaf)
22571 * @since 1.0.3
22572 * @uses FS_Api
22573 */
22574 private function verify_email() {
22575 $this->_handle_account_user_sync();
22576
22577 if ( $this->_user->is_verified() ) {
22578 return;
22579 }
22580
22581 $api = $this->get_api_site_scope();
22582 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22583 'after_email_confirm_url' => $this->_get_admin_page_url(
22584 'account',
22585 array( 'fs_action' => 'sync_user' )
22586 )
22587 ) );
22588
22589 if ( ! isset( $result->error ) ) {
22590 $this->_admin_notices->add( sprintf(
22591 $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' ),
22592 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22593 ) );
22594 } else {
22595 // handle different error cases.
22596
22597 }
22598 }
22599
22600 /**
22601 * @author Vova Feldman (@svovaf)
22602 * @since 1.1.2
22603 *
22604 * @param array $params
22605 * @param bool|null $network
22606 *
22607 * @return string
22608 */
22609 function get_activation_url( $params = array(), $network = null ) {
22610 if ( $this->is_addon() && $this->has_free_plan() ) {
22611 /**
22612 * @author Vova Feldman (@svovaf)
22613 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22614 */
22615 return $this->get_parent_instance()->get_activation_url( $params );
22616 }
22617
22618 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22619 }
22620
22621 /**
22622 * @author Vova Feldman (@svovaf)
22623 * @since 1.2.1.5
22624 *
22625 * @param array $params
22626 *
22627 * @return string
22628 */
22629 function get_reconnect_url( $params = array() ) {
22630 $params['fs_action'] = 'reset_anonymous_mode';
22631 $params['fs_unique_affix'] = $this->get_unique_affix();
22632
22633 return $this->get_activation_url( $params );
22634 }
22635
22636 /**
22637 * Get the URL of the page that should be loaded after the user connect
22638 * or skip in the opt-in screen.
22639 *
22640 * @author Vova Feldman (@svovaf)
22641 * @since 1.1.3
22642 *
22643 * @param string $filter Filter name.
22644 * @param array $params Since 1.2.2.7
22645 * @param bool|null $network
22646 *
22647 * @return string
22648 */
22649 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22650 if ( $this->show_opt_in_on_themes_page() &&
22651 ( fs_request_has( 'pending_activation' ) ||
22652 // For cases when the first time path is set, even though it's a WP.org theme.
22653 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22654 ) {
22655 $first_time_path = '';
22656 } else {
22657 $first_time_path = $this->_menu->get_first_time_path(
22658 fs_is_network_admin() && $this->_is_network_active
22659 );
22660 }
22661
22662 if ( $this->_is_network_active &&
22663 fs_is_network_admin() &&
22664 ! $this->_menu->has_network_menu() &&
22665 $this->is_network_registered()
22666 ) {
22667 $target_url = $this->get_account_url();
22668 } else {
22669 // Default plugin's page.
22670 $target_url = $this->_get_admin_page_url( '', array(), $network );
22671 }
22672
22673 return add_query_arg( $params, $this->apply_filters(
22674 $filter,
22675 empty( $first_time_path ) ?
22676 $target_url :
22677 $first_time_path
22678 ) );
22679 }
22680
22681 /**
22682 * Handle account page updates / edits / actions.
22683 *
22684 * @author Vova Feldman (@svovaf)
22685 * @since 1.0.2
22686 *
22687 */
22688 private function _handle_account_edits() {
22689 if ( ! $this->is_user_admin() ) {
22690 return;
22691 }
22692
22693 $action = fs_get_action();
22694
22695 if ( empty( $action ) ) {
22696 return;
22697 }
22698
22699 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22700 $install_id = fs_request_get( 'install_id', '' );
22701
22702 // Alias.
22703 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22704
22705 $is_network_action = $this->is_network_level_action();
22706 $blog_id = $this->is_network_level_site_specific_action();
22707 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22708
22709 if ( is_numeric( $blog_id ) ) {
22710 $this->switch_to_blog( $blog_id );
22711 } else {
22712 $blog_id = '';
22713 }
22714
22715 switch ( $action ) {
22716 case 'opt_in':
22717 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22718
22719 if ( $is_parent_plugin_action ) {
22720 if ( $is_network_action && ! empty( $blog_id ) ) {
22721 if ( ! $this->is_registered() ) {
22722 $this->install_with_user(
22723 $this->get_network_user(),
22724 false,
22725 false,
22726 false,
22727 false
22728 );
22729
22730 $this->_admin_notices->add(
22731 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22732 $this->get_text_inline( 'Awesome', 'awesome' )
22733 );
22734 }
22735 }
22736 }
22737 break;
22738
22739 case 'toggle_tracking':
22740 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22741
22742 if ( $is_parent_plugin_action ) {
22743 if ( $is_network_action && ! empty( $blog_id ) ) {
22744 if ( $this->is_registered( true ) ) {
22745 if ( $this->is_tracking_prohibited( $blog_id ) ) {
22746 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
22747 $this->_admin_notices->add(
22748 sprintf( $this->get_text_inline( 'Sharing diagnostic data with %s helps to provide functionality that\'s more relevant to your website, avoid WordPress or PHP version incompatibilities that can break your website, and recognize which languages & regions the plugin should be translated and tailored to.', 'opt-out-message-appreciation' ), "<b>{$this->get_plugin_title()}</b>" ),
22749 $this->get_text_inline( 'Thank you!', 'thank-you' )
22750 );
22751 }
22752 } else {
22753 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
22754 $install = $this->get_install_by_blog_id( $blog_id );
22755
22756 $this->_admin_notices->add(
22757 sprintf(
22758 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
22759 self::get_unfiltered_site_url( $blog_id, true ),
22760 "<b>{$this->get_plugin_title()}</b>"
22761 )
22762 );
22763 }
22764 }
22765 }
22766 }
22767 }
22768
22769 break;
22770
22771 case 'delete_account':
22772 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22773
22774 $is_network_deletion = $is_network_action && empty( $blog_id );
22775
22776 if ( $is_parent_plugin_action ) {
22777 // Delete add-on installs if have any.
22778 $installed_addons = $this->get_installed_addons();
22779 foreach ( $installed_addons as $fs_addon ) {
22780 if ( $is_network_deletion ) {
22781 $fs_addon->delete_network_account_event();
22782 } else {
22783 $fs_addon->delete_account_event();
22784 }
22785 }
22786
22787 if ( $is_network_deletion ) {
22788 $this->delete_network_account_event();
22789 } else {
22790 $this->delete_account_event();
22791 }
22792
22793 // Clear user and site.
22794 $this->_site = null;
22795 $this->_user = null;
22796
22797 $this->maybe_set_slug_and_network_menu_exists_flag();
22798
22799 fs_redirect( $this->get_activation_url() );
22800 } else {
22801 if ( $this->is_addon_activated( $plugin_id ) ) {
22802 $fs_addon = self::get_instance_by_id( $plugin_id );
22803
22804 if ( $is_network_deletion ) {
22805 $fs_addon->delete_network_account_event();
22806 } else {
22807 $fs_addon->delete_account_event();
22808 }
22809
22810 fs_redirect( $this->_get_admin_page_url( 'account' ) );
22811 }
22812 }
22813
22814 return;
22815
22816 case 'downgrade_account':
22817 if ( is_numeric( $blog_id ) ) {
22818 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22819 } else {
22820 check_admin_referer( $action );
22821 }
22822
22823 $switch_to_network_install_blog_after_cancellation = (
22824 is_numeric( $blog_id ) &&
22825 $plugin_id == $this->get_id() &&
22826 ! $this->is_trial()
22827 );
22828
22829 $result = $this->cancel_subscription_or_trial( $plugin_id );
22830 if ( $this->is_api_error( $result ) ) {
22831 $this->_admin_notices->add(
22832 $result->error->message,
22833 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22834 'error'
22835 );
22836 }
22837
22838 if ( $switch_to_network_install_blog_after_cancellation ) {
22839 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22840 }
22841
22842 return;
22843
22844 case 'activate_license':
22845 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22846
22847 $fs = $this;
22848 if ( $plugin_id != $this->get_id() ) {
22849 $fs = $this->is_addon_activated( $plugin_id ) ?
22850 self::get_instance_by_id( $plugin_id ) :
22851 null;
22852 }
22853
22854 if ( is_object( $fs ) ) {
22855 $fs->_activate_license();
22856
22857 /**
22858 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
22859 *
22860 * @author Leo Fajardo (@leorw)
22861 * @since 2.4.0
22862 */
22863 unset( $_REQUEST['plugin_id'] );
22864
22865 if ( $this->is_bundle_license_auto_activation_enabled() ) {
22866 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
22867 }
22868 }
22869
22870 return;
22871
22872 case 'deactivate_license':
22873 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22874
22875 if ( $plugin_id == $this->get_id() ) {
22876 $this->_deactivate_license();
22877
22878 if ( $this->is_only_premium() ) {
22879 // Clear user and site.
22880 $this->_site = null;
22881 $this->_user = null;
22882
22883 if ( ! $is_network_action ) {
22884 fs_redirect( $this->get_activation_url() );
22885 } else if ( is_numeric( $blog_id ) ) {
22886 $this->switch_to_blog( $this->_storage->network_install_blog_id );
22887 }
22888 }
22889 } else {
22890 if ( $this->is_addon_activated( $plugin_id ) ) {
22891 $fs_addon = self::get_instance_by_id( $plugin_id );
22892 $fs_addon->_deactivate_license();
22893 }
22894 }
22895
22896 return;
22897
22898 case 'check_updates':
22899 check_admin_referer( $action );
22900 $this->check_updates();
22901
22902 return;
22903
22904 case 'change_owner':
22905 $state = fs_request_get( 'state', 'init' );
22906 switch ( $state ) {
22907 case 'init':
22908 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
22909 check_admin_referer( 'change_owner' );
22910
22911 $candidate_email = fs_request_get( 'candidate_email' );
22912 $transfer_type = fs_request_get( 'transfer_type' );
22913
22914 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
22915 if ( 'transfer' === $transfer_type ) {
22916 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. The email owner must confirm the update within the next 4 hours.', 'change-owner-request-sent-x-transfer' ), '<b>' . $this->_user->email . '</b>' ) );
22917 } else {
22918 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. You must confirm the update within the next 4 hours. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '<b>' . $this->_user->email . '</b>' ) );
22919 }
22920 }
22921 break;
22922 case 'owner_confirmed':
22923 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
22924 $candidate_email = fs_request_get( 'candidate_email', '' );
22925
22926 if ( ! is_email($candidate_email ) ) {
22927 return;
22928 }
22929
22930 $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>' ) );
22931 break;
22932 case 'candidate_confirmed':
22933 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
22934 if ( $this->complete_change_owner() ) {
22935 $this->_admin_notices->add_sticky(
22936 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
22937 'ownership_changed',
22938 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
22939 );
22940 } else {
22941 // @todo Handle failed ownership change message.
22942 }
22943 break;
22944 }
22945
22946 return;
22947
22948 case 'update_user_name':
22949 check_admin_referer( 'update_user_name' );
22950
22951 $result = $this->update_user_name();
22952
22953 if ( isset( $result->error ) ) {
22954 $this->_admin_notices->add(
22955 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
22956 $oops_text,
22957 'error'
22958 );
22959 } else {
22960 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
22961 }
22962
22963 return;
22964
22965 #region Actions that might be called from external links (e.g. email)
22966
22967 /**
22968 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
22969 */
22970
22971 case 'cancel_trial':
22972 $result = $this->cancel_subscription_or_trial( $plugin_id );
22973 if ( $this->is_api_error( $result ) ) {
22974 $this->_admin_notices->add(
22975 $result->error->message,
22976 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
22977 'error'
22978 );
22979 }
22980
22981 return;
22982
22983 case 'verify_email':
22984 $this->verify_email();
22985
22986 return;
22987
22988 case 'sync_user':
22989 $this->_handle_account_user_sync();
22990
22991 return;
22992
22993 case $this->get_unique_affix() . '_sync_license':
22994 $this->_sync_license();
22995
22996 return;
22997
22998 case 'download_latest':
22999 $this->download_latest_directly( $plugin_id );
23000
23001 return;
23002
23003 #endregion
23004 }
23005
23006 if ( WP_FS__IS_POST_REQUEST ) {
23007 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23008 foreach ( $properties as $p ) {
23009 if ( 'update_' . $p === $action ) {
23010 check_admin_referer( $action );
23011
23012 $this->_logger->log( $action );
23013
23014 $site_property = substr( $p, strlen( 'site_' ) );
23015 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23016 $this->get_site()->{$site_property} = $site_property_value;
23017
23018 // Store account after modification.
23019 $this->_store_site();
23020
23021 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23022
23023 $this->_admin_notices->add( sprintf(
23024 /* translators: %s: User's account property (e.g. email address, name) */
23025 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23026 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23027 ) );
23028
23029 return;
23030 }
23031 }
23032 }
23033 }
23034
23035 /**
23036 * Adds CSS classes for the body tag in the admin.
23037 *
23038 * @param string $classes Space-separated string of class names.
23039 *
23040 * @return string $classes FS Admin body tag class names.
23041 */
23042 public function fs_addons_body_class( $classes ) {
23043 $classes .= ' plugins-php';
23044 return $classes;
23045 }
23046
23047 /**
23048 * Account page resources load.
23049 *
23050 * @author Vova Feldman (@svovaf)
23051 * @since 1.0.6
23052 */
23053 function _account_page_load() {
23054 $this->_logger->entrance();
23055
23056 $this->_logger->info( var_export( $_REQUEST, true ) );
23057
23058 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23059
23060 if ( $this->has_addons() ) {
23061 wp_enqueue_script( 'plugin-install' );
23062 add_thickbox();
23063 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23064 }
23065
23066 if ( $this->has_paid_plan() &&
23067 ! $this->has_any_license() &&
23068 ! $this->is_sync_executed() &&
23069 $this->is_tracking_allowed()
23070 ) {
23071 /**
23072 * If no licenses found and no sync job was executed during the last 24 hours,
23073 * just execute the sync job right away (blocking execution).
23074 *
23075 * @since 1.1.7.3
23076 */
23077 $this->run_manual_sync();
23078 }
23079
23080 $this->_handle_account_edits();
23081
23082 if (
23083 is_object( $this->_license ) &&
23084 $this->_license->user_id == $this->_user->id &&
23085 ! $this->is_whitelabeled( true )
23086 ) {
23087 $this->_admin_notices->add(
23088 sprintf(
23089 $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' ),
23090 sprintf(
23091 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23092 $this->get_text_inline( 'Click here', 'click-here' )
23093 )
23094 ),
23095 '',
23096 'success',
23097 false,
23098 'license_not_whitelabeled'
23099 );
23100 }
23101
23102 $this->do_action( 'account_page_load_before_departure' );
23103 }
23104
23105 /**
23106 * Renders the "Affiliation" page.
23107 *
23108 * @author Leo Fajardo (@leorw)
23109 * @since 1.2.3
23110 */
23111 function _affiliation_page_render() {
23112 $this->_logger->entrance();
23113
23114 $this->fetch_affiliate_and_terms();
23115
23116 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23117
23118 $is_bundle_context = $this->has_bundle_context();
23119
23120 $plugin_title = $this->get_plugin_title();
23121
23122 if ( $is_bundle_context ) {
23123 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23124
23125 // Add the suffix "Bundle" only if the word is not present in the title itself.
23126 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23127 $plugin_title = $this->apply_filters(
23128 'formatted_bundle_title',
23129 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23130 );
23131 }
23132 }
23133
23134 $vars = array(
23135 'id' => $this->_module_id,
23136 'plugin_title' => $plugin_title,
23137 );
23138 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23139 }
23140
23141
23142 /**
23143 * Render account page.
23144 *
23145 * @author Vova Feldman (@svovaf)
23146 * @since 1.0.0
23147 */
23148 function _account_page_render() {
23149 $this->_logger->entrance();
23150
23151 $template = 'account.php';
23152 $vars = array( 'id' => $this->_module_id );
23153
23154 /**
23155 * Added filter to the template to allow developers wrapping the template
23156 * in custom HTML (e.g. within a wizard/tabs).
23157 *
23158 * @author Vova Feldman (@svovaf)
23159 * @since 1.2.1.6
23160 */
23161 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23162 }
23163
23164 /**
23165 * Render account connect page.
23166 *
23167 * @author Vova Feldman (@svovaf)
23168 * @since 1.0.7
23169 */
23170 function _connect_page_render() {
23171 $this->_logger->entrance();
23172
23173 $vars = array( 'id' => $this->_module_id );
23174
23175 /**
23176 * Added filter to the template to allow developers wrapping the template
23177 * in custom HTML (e.g. within a wizard/tabs).
23178 *
23179 * @author Vova Feldman (@svovaf)
23180 * @since 1.2.1.6
23181 */
23182 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23183 }
23184
23185 /**
23186 * Load required resources before add-ons page render.
23187 *
23188 * @author Vova Feldman (@svovaf)
23189 * @since 1.0.6
23190 */
23191 function _addons_page_load() {
23192 $this->_logger->entrance();
23193
23194 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23195
23196 wp_enqueue_script( 'plugin-install' );
23197 add_thickbox();
23198 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23199
23200 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23201 $this->_admin_notices->add(
23202 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>' ),
23203 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23204 'update-nag'
23205 );
23206 }
23207 }
23208
23209 /**
23210 * Render add-ons page.
23211 *
23212 * @author Vova Feldman (@svovaf)
23213 * @since 1.0.6
23214 */
23215 function _addons_page_render() {
23216 $this->_logger->entrance();
23217
23218 $vars = array( 'id' => $this->_module_id );
23219
23220 /**
23221 * Added filter to the template to allow developers wrapping the template
23222 * in custom HTML (e.g. within a wizard/tabs).
23223 *
23224 * @author Vova Feldman (@svovaf)
23225 * @since 1.2.1.6
23226 */
23227 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23228 }
23229
23230 /* Pricing & Upgrade
23231 ------------------------------------------------------------------------------------------------------------------*/
23232 /**
23233 * Render pricing page.
23234 *
23235 * @author Vova Feldman (@svovaf)
23236 * @since 1.0.0
23237 */
23238 function _pricing_page_render() {
23239 $this->_logger->entrance();
23240
23241 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23242 fs_enqueue_local_style( 'fs_checkout', '/admin/checkout.css' );
23243
23244 $vars = array( 'id' => $this->_module_id );
23245
23246 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23247 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23248 } else {
23249 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23250 }
23251 }
23252
23253 /**
23254 * @author Leo Fajardo (@leorw)
23255 * @since 2.3.1
23256 */
23257 function _add_pricing_ajax_handler() {
23258 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23259 }
23260
23261 /**
23262 * @author Leo Fajardo (@leorw)
23263 * @since 2.3.1
23264 */
23265 function _fs_pricing_ajax_action_handler() {
23266 $this->check_ajax_referer( 'pricing_ajax_action' );
23267
23268 $result = null;
23269 $pricing_action = fs_request_get( 'pricing_action' );
23270
23271 switch ( $pricing_action ) {
23272 case 'fetch_pricing_data':
23273 $params = array(
23274 'is_enriched' => true,
23275 'trial' => fs_request_get_bool( 'trial' ),
23276 'sandbox' => fs_request_get_raw( 'sandbox' ),
23277 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23278 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23279 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23280 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23281 );
23282
23283 $bundle_id = $this->get_bundle_id();
23284 $bundle_public_key = $this->get_bundle_public_key();
23285
23286 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23287
23288 if ( ! $has_bundle_context ) {
23289 $api = $this->get_api_plugin_scope();
23290 } else {
23291 $api = FS_Api::instance(
23292 $bundle_id,
23293 'plugin',
23294 $bundle_id,
23295 $bundle_public_key,
23296 ! $this->is_live(),
23297 false,
23298 $this->get_sdk_version()
23299 );
23300
23301 $params['plugin_id'] = $this->get_id();
23302 $params['plugin_public_key'] = $this->get_public_key();
23303 }
23304
23305 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23306 break;
23307 case 'start_trial':
23308 $trial_plan_id = fs_request_get( 'plan_id' );
23309
23310 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23311 $plan = $this->_get_plan_by_id( $trial_plan_id );
23312
23313 if ( ! $plan ) {
23314 $this->shoot_ajax_failure( 'Invalid plan ID.' );
23315 return;
23316 }
23317
23318 $result = $this->start_trial( $plan->name, true );
23319 } else {
23320 // @todo - This fails for sandbox trial at the moment if the trial was already utilized.
23321 $result = $this->opt_in(
23322 false,
23323 false,
23324 false,
23325 false,
23326 false,
23327 $trial_plan_id
23328 );
23329 }
23330 }
23331
23332 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23333 $this->_logger->api_error( $result );
23334
23335 self::shoot_ajax_failure(
23336 isset( $result->error ) ?
23337 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23338 var_export( $result, true )
23339 );
23340 }
23341
23342 $this->shoot_ajax_success( $result );
23343 }
23344
23345 #----------------------------------------------------------------------------------
23346 #region Contact Us
23347 #----------------------------------------------------------------------------------
23348
23349 /**
23350 * Render contact-us page.
23351 *
23352 * @author Vova Feldman (@svovaf)
23353 * @since 1.0.3
23354 */
23355 function _contact_page_render() {
23356 $this->_logger->entrance();
23357
23358 $vars = array( 'id' => $this->_module_id );
23359
23360 /**
23361 * Added filter to the template to allow developers wrapping the template
23362 * in custom HTML (e.g. within a wizard/tabs).
23363 *
23364 * @author Vova Feldman (@svovaf)
23365 * @since 2.1.3
23366 */
23367 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23368 }
23369
23370 #endregion ------------------------------------------------------------------------
23371
23372 /**
23373 * Hide all admin notices to prevent distractions.
23374 *
23375 * @author Vova Feldman (@svovaf)
23376 * @since 1.0.3
23377 *
23378 * @uses remove_all_actions()
23379 */
23380 private static function _hide_admin_notices() {
23381 remove_all_actions( 'admin_notices' );
23382 remove_all_actions( 'network_admin_notices' );
23383 remove_all_actions( 'all_admin_notices' );
23384 remove_all_actions( 'user_admin_notices' );
23385 }
23386
23387 static function _clean_admin_content_section_hook() {
23388 $hide_admin_notices = true;
23389
23390 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23391 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23392
23393 $hide_admin_notices = false;
23394 }
23395
23396 if ( $hide_admin_notices ) {
23397 self::_hide_admin_notices();
23398 }
23399
23400 // Hide footer.
23401 echo '<style>#wpfooter { display: none !important; }</style>';
23402 }
23403
23404 /**
23405 * Attach to admin_head hook to hide all admin notices.
23406 *
23407 * @author Vova Feldman (@svovaf)
23408 * @since 1.0.3
23409 */
23410 static function _clean_admin_content_section() {
23411 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23412 }
23413
23414 /* CSS & JavaScript
23415 ------------------------------------------------------------------------------------------------------------------*/
23416 /* function _enqueue_script($handle, $src) {
23417 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23418
23419 $this->_logger->entrance( 'script = ' . $url );
23420
23421 wp_enqueue_script( $handle, $url );
23422 }*/
23423
23424 /* SDK
23425 ------------------------------------------------------------------------------------------------------------------*/
23426 private $_user_api;
23427
23428 /**
23429 *
23430 * @author Vova Feldman (@svovaf)
23431 * @since 1.0.2
23432 *
23433 * @param bool $flush
23434 *
23435 * @return FS_Api
23436 */
23437 function get_api_user_scope( $flush = false ) {
23438 if ( ! isset( $this->_user_api ) || $flush ) {
23439 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23440 }
23441
23442 return $this->_user_api;
23443 }
23444
23445 /**
23446 * @author Vova Feldman (@svovaf)
23447 * @since 2.0.0
23448 *
23449 * @param \FS_User $user
23450 *
23451 * @return \FS_Api
23452 */
23453 private function get_api_user_scope_by_user( FS_User $user ) {
23454 return FS_Api::instance(
23455 $this->_module_id,
23456 'user',
23457 $user->id,
23458 $user->public_key,
23459 ! $this->is_live(),
23460 $user->secret_key,
23461 $this->get_sdk_version()
23462 );
23463 }
23464
23465 /**
23466 *
23467 * @author Leo Fajardo (@leorw)
23468 * @since 2.0.0
23469 *
23470 * @param bool $flush
23471 *
23472 * @return FS_Api
23473 */
23474 private function get_current_or_network_user_api_scope( $flush = false ) {
23475 if ( ! $this->_is_network_active ||
23476 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23477 ) {
23478 return $this->get_api_user_scope( $flush );
23479 }
23480
23481 $user = $this->get_current_or_network_user();
23482
23483 $this->_user_api = FS_Api::instance(
23484 $this->_module_id,
23485 'user',
23486 $user->id,
23487 $user->public_key,
23488 ! $this->is_live(),
23489 $user->secret_key,
23490 $this->get_sdk_version()
23491 );
23492
23493 return $this->_user_api;
23494 }
23495
23496 private $_site_api;
23497
23498 /**
23499 *
23500 * @author Vova Feldman (@svovaf)
23501 * @since 1.0.2
23502 *
23503 * @param bool $flush
23504 *
23505 * @return FS_Api
23506 */
23507 private function get_api_site_scope( $flush = false ) {
23508 if ( ! isset( $this->_site_api ) || $flush ) {
23509 $this->_site_api = FS_Api::instance(
23510 $this->_module_id,
23511 'install',
23512 $this->_site->id,
23513 $this->_site->public_key,
23514 ! $this->is_live(),
23515 $this->_site->secret_key,
23516 $this->get_sdk_version(),
23517 self::get_unfiltered_site_url()
23518 );
23519 }
23520
23521 return $this->_site_api;
23522 }
23523
23524 /**
23525 * @author Leo Fajardo (@leorw)
23526 * @since 2.5.0
23527 *
23528 * @param string $path
23529 * @param string $method
23530 * @param array $params
23531 * @param bool $flush_instance
23532 *
23533 * @return array|mixed|string|void
23534 * @throws Freemius_Exception
23535 */
23536 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23537 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23538
23539 /**
23540 * Checks if the local install's URL is different from the remote install's URL, update the local install if necessary, and then run the clone handler if the install's URL is different from the URL of the site.
23541 *
23542 * @author Leo Fajardo (@leorw)
23543 * @since 2.5.0
23544 */
23545 if (
23546 $this->is_registered() &&
23547 FS_Api::is_api_result_entity( $result ) &&
23548 isset( $result->url )
23549 ) {
23550 $stored_local_url = trailingslashit( $this->_site->url );
23551 $stored_remote_url = trailingslashit( $result->url );
23552
23553 if ( $stored_local_url !== $stored_remote_url ) {
23554 $this->_site->url = $result->url;
23555 $this->_store_site();
23556 }
23557
23558 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23559 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23560 }
23561 }
23562
23563 return $result;
23564 }
23565
23566 private $_plugin_api;
23567
23568 /**
23569 * Get plugin public API scope.
23570 *
23571 * @author Vova Feldman (@svovaf)
23572 * @since 1.0.7
23573 *
23574 * @return FS_Api
23575 */
23576 function get_api_plugin_scope() {
23577 if ( ! isset( $this->_plugin_api ) ) {
23578 $this->_plugin_api = FS_Api::instance(
23579 $this->_module_id,
23580 'plugin',
23581 $this->_plugin->id,
23582 $this->_plugin->public_key,
23583 ! $this->is_live(),
23584 false,
23585 $this->get_sdk_version()
23586 );
23587 }
23588
23589 return $this->_plugin_api;
23590 }
23591
23592 /**
23593 * Get bundle public API scope.
23594 *
23595 * @author Vova Feldman (@svovaf)
23596 * @since 2.3.1
23597 *
23598 * @return FS_Api
23599 */
23600 function get_api_bundle_scope() {
23601 return FS_Api::instance(
23602 $this->get_bundle_id(),
23603 'plugin',
23604 $this->get_bundle_id(),
23605 $this->get_bundle_public_key(),
23606 ! $this->is_live(),
23607 false,
23608 $this->get_sdk_version()
23609 );
23610 }
23611
23612 /**
23613 * Get site API scope object (fallback to public plugin scope when not registered).
23614 *
23615 * @author Vova Feldman (@svovaf)
23616 * @since 1.0.7
23617 *
23618 * @return FS_Api
23619 */
23620 function get_api_site_or_plugin_scope() {
23621 return $this->is_registered() ?
23622 $this->get_api_site_scope() :
23623 $this->get_api_plugin_scope();
23624 }
23625
23626 /**
23627 * @author Leo Fajardo (@leorw)
23628 * @since 2.2.3.1
23629 *
23630 * @param object $result
23631 */
23632 private function maybe_modify_api_curl_error_message( $result ) {
23633 if (
23634 'cUrlMissing' !== $result->error->type &&
23635 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23636 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23637 ) {
23638 return;
23639 }
23640
23641 $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' ) .
23642 ' ' .
23643 $this->esc_html_inline(
23644 sprintf(
23645 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23646 implode(
23647 ', ',
23648 $this->apply_filters( 'api_domains', array(
23649 'api.freemius.com',
23650 'wp.freemius.com'
23651 ) )
23652 )
23653 ),
23654 'connectivity-whitelist'
23655 ) .
23656 ' ' .
23657 sprintf(
23658 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23659 $this->get_module_type()
23660 );
23661 }
23662
23663 /**
23664 * Show trial promotional notice (if any trial exist).
23665 *
23666 * @author Vova Feldman (@svovaf)
23667 * @since 1.0.9
23668 *
23669 * @param FS_Plugin_Plan[] $plans
23670 */
23671 function _check_for_trial_plans( $plans ) {
23672 /**
23673 * 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.
23674 *
23675 * @author Vova Feldman (@svovaf)
23676 * @since 2.1.2
23677 */
23678 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23679 $plans = array( $plans );
23680 }
23681
23682 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23683 $plans = array();
23684 }
23685
23686 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23687 }
23688
23689 /**
23690 * During trial promotion the "upgrade" submenu item turns to
23691 * "start trial" to encourage the trial. Since we want to keep
23692 * the same menu item handler and there's no robust way to
23693 * add new arguments to the menu item link's querystring,
23694 * use JavaScript to find the menu item and update the href of
23695 * the link.
23696 *
23697 * @author Vova Feldman (@svovaf)
23698 * @since 1.2.1.5
23699 */
23700 function _fix_start_trial_menu_item_url() {
23701 $template_args = array( 'id' => $this->_module_id );
23702 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23703 }
23704
23705 /**
23706 * Check if module is currently in a trial promotion mode.
23707 *
23708 * @author Vova Feldman (@svovaf)
23709 * @since 1.2.2.7
23710 *
23711 * @return bool
23712 */
23713 function is_in_trial_promotion() {
23714 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23715 }
23716
23717 /**
23718 * Show trial promotional notice (if any trial exist).
23719 *
23720 * @author Vova Feldman (@svovaf)
23721 * @since 1.0.9
23722 *
23723 * @return bool If trial notice added.
23724 */
23725 function _add_trial_notice() {
23726 if ( ! $this->is_user_admin() ) {
23727 return false;
23728 }
23729
23730 if ( ! $this->is_user_in_admin() ) {
23731 return false;
23732 }
23733
23734 if ( $this->_is_network_active ) {
23735 if ( fs_is_network_admin() ) {
23736 // Network level trial is disabled at the moment.
23737 return false;
23738 }
23739
23740 if ( ! $this->is_delegated_connection() ) {
23741 // Only delegated sites should support trials.
23742 return false;
23743 }
23744 }
23745
23746 // Check if trial message is already shown.
23747 if ( $this->is_in_trial_promotion() ) {
23748 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23749
23750 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23751
23752 return false;
23753 }
23754
23755 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
23756 // Don't show trial if running the premium code, unless running in DEV mode.
23757 return false;
23758 }
23759
23760 if ( ! $this->has_trial_plan() ) {
23761 // No plans with trial.
23762 return false;
23763 }
23764
23765 if ( ! $this->apply_filters( 'show_trial', true ) ) {
23766 // Developer explicitly asked not to show the trial promo.
23767 return false;
23768 }
23769
23770 if ( $this->is_registered() ) {
23771 // Check if trial already utilized.
23772 if ( $this->_site->is_trial_utilized() ) {
23773 return false;
23774 }
23775
23776 if ( $this->is_paying_or_trial() ) {
23777 // Don't show trial if paying or already in trial.
23778 return false;
23779 }
23780 }
23781
23782 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23783 // If not yet opted-in/skipped, or pending activation, don't show trial.
23784 return false;
23785 }
23786
23787 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
23788 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
23789
23790 // Show promotion if never shown before and 24 hours after initial activation with FS.
23791 if ( ! $was_promotion_shown_before &&
23792 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
23793 ) {
23794 return false;
23795 }
23796
23797 // OR if promotion was shown before, try showing it every 30 days.
23798 if ( $was_promotion_shown_before &&
23799 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
23800 ) {
23801 return false;
23802 }
23803
23804 $trial_period = $this->_trial_days;
23805 $require_payment = $this->_is_trial_require_payment;
23806 $trial_url = $this->get_trial_url();
23807 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
23808
23809 if ( $this->is_registered() ) {
23810 // If opted-in, override trial with up to date data from API.
23811 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
23812 $trial_plans_count = count( $trial_plans );
23813
23814 if ( 0 === $trial_plans_count ) {
23815 // If there's no plans with a trial just exit.
23816 return false;
23817 }
23818
23819 /**
23820 * @var FS_Plugin_Plan $paid_plan
23821 */
23822 $paid_plan = $trial_plans[0];
23823 $require_payment = $paid_plan->is_require_subscription;
23824 $trial_period = $paid_plan->trial_period;
23825
23826 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
23827
23828 if ( $total_paid_plans !== $trial_plans_count ) {
23829 // Not all paid plans have a trial - generate a string of those that have it.
23830 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
23831 $plans_string .= sprintf(
23832 ' <a href="%s">%s</a>',
23833 $trial_url,
23834 $trial_plans[ $i ]->title
23835 );
23836
23837 if ( $i < $trial_plans_count - 2 ) {
23838 $plans_string .= ', ';
23839 } else if ( $i == $trial_plans_count - 2 ) {
23840 $plans_string .= ' and ';
23841 }
23842 }
23843 }
23844 }
23845
23846 $message = sprintf(
23847 $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' ),
23848 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
23849 $plans_string,
23850 $trial_period
23851 );
23852
23853 // "No Credit-Card Required" or "No Commitment for N Days".
23854 $cc_string = $require_payment ?
23855 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
23856 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
23857
23858
23859 // Start trial button.
23860 $button = ' ' . sprintf(
23861 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
23862 $trial_url,
23863 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
23864 );
23865
23866 $this->_admin_notices->add_sticky(
23867 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
23868 'trial_promotion',
23869 '',
23870 'promotion'
23871 );
23872
23873 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
23874
23875 return true;
23876 }
23877
23878 /**
23879 * Lets users/customers know that the product has an affiliate program.
23880 *
23881 * @author Leo Fajardo (@leorw)
23882 * @since 1.2.2.11
23883 *
23884 * @return bool Returns true if the notice has been added.
23885 */
23886 function _add_affiliate_program_notice() {
23887 if ( ! $this->is_user_admin() ) {
23888 return false;
23889 }
23890
23891 if ( ! $this->is_user_in_admin() ) {
23892 return false;
23893 }
23894
23895 // Check if the notice is already shown.
23896 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
23897 return false;
23898 }
23899
23900 if (
23901 // Product has no affiliate program.
23902 ! $this->has_affiliate_program() ||
23903 // User has applied for an affiliate account.
23904 ! empty( $this->_storage->affiliate_application_data )
23905 ) {
23906 return false;
23907 }
23908
23909 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
23910 // Developer explicitly asked not to show the notice about the affiliate program.
23911 return false;
23912 }
23913
23914 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23915 // If not yet opted in/skipped, or pending activation, don't show the notice.
23916 return false;
23917 }
23918
23919 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
23920 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
23921
23922 /**
23923 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
23924 * activation with FS.
23925 */
23926 if ( $was_notice_shown_before ||
23927 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
23928 ) {
23929 return false;
23930 }
23931
23932 if ( ! $this->is_paying() &&
23933 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
23934 ) {
23935 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
23936 return false;
23937 }
23938
23939 $message = sprintf(
23940 $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' ),
23941 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
23942 $this->get_module_label( true )
23943 );
23944
23945 // HTML code for the "Learn more..." button.
23946 $button = ' ' . sprintf(
23947 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
23948 $this->_get_admin_page_url( 'affiliation' ),
23949 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
23950 );
23951
23952 $this->_admin_notices->add_sticky(
23953 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
23954 'affiliate_program',
23955 '',
23956 'promotion'
23957 );
23958
23959 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
23960
23961 return true;
23962 }
23963
23964 /**
23965 * @author Vova Feldman (@svovaf)
23966 * @since 1.2.1.5
23967 */
23968 function _enqueue_common_css() {
23969 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
23970 // Add basic CSS for admin-notices and menu-item colors.
23971 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23972 }
23973 }
23974
23975 /**
23976 * @author Leo Fajardo (@leorw)
23977 * @since 1.2.2
23978 */
23979 function _show_theme_activation_optin_dialog() {
23980 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
23981
23982 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
23983 }
23984
23985 /**
23986 * @author Leo Fajardo (@leorw)
23987 * @since 1.2.2
23988 */
23989 function _add_fs_theme_activation_dialog() {
23990 global $pagenow;
23991
23992 if ( 'themes.php' !== $pagenow ) {
23993 return;
23994 }
23995
23996 $vars = array( 'id' => $this->_module_id );
23997 fs_require_once_template( 'connect.php', $vars );
23998 }
23999
24000 /* Action Links
24001 ------------------------------------------------------------------------------------------------------------------*/
24002 private $_action_links_hooked = false;
24003 private $_action_links = array();
24004
24005 /**
24006 * Hook to plugin action links filter.
24007 *
24008 * @author Vova Feldman (@svovaf)
24009 * @since 1.0.0
24010 */
24011 private function hook_plugin_action_links() {
24012 $this->_logger->entrance();
24013
24014 $this->_action_links_hooked = true;
24015
24016 $this->_logger->log( 'Adding action links hooks.' );
24017
24018 // Add action link to settings page.
24019 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24020 &$this,
24021 '_modify_plugin_action_links_hook'
24022 ), WP_FS__DEFAULT_PRIORITY, 2 );
24023 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24024 &$this,
24025 '_modify_plugin_action_links_hook'
24026 ), WP_FS__DEFAULT_PRIORITY, 2 );
24027 }
24028
24029 /**
24030 * Add plugin action link.
24031 *
24032 * @author Vova Feldman (@svovaf)
24033 * @since 1.0.0
24034 *
24035 * @param $label
24036 * @param $url
24037 * @param bool $external
24038 * @param int $priority
24039 * @param bool $key
24040 */
24041 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24042 $this->_logger->entrance();
24043
24044 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24045 $this->_action_links[ $priority ] = array();
24046 }
24047
24048 if ( false === $key ) {
24049 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24050 }
24051
24052 $this->_action_links[ $priority ][] = array(
24053 'label' => $label,
24054 'href' => $url,
24055 'key' => $key,
24056 'external' => $external
24057 );
24058 }
24059
24060 /**
24061 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24062 *
24063 * @author Vova Feldman (@svovaf)
24064 * @since 1.0.0
24065 */
24066 function _add_upgrade_action_link() {
24067 $this->_logger->entrance();
24068
24069 $is_activation_mode = $this->is_activation_mode();
24070
24071 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24072
24073 /**
24074 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24075 * and "Add-Ons" menus should be added.
24076 *
24077 * @author Leo Fajardo (@leorw)
24078 * @since 2.3.0
24079 */
24080 $add_upgrade_link = (
24081 $add_action_links ||
24082 ( $is_activation_mode && $this->is_only_premium() )
24083 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24084
24085 $add_addons_link = ( $add_action_links && $this->has_addons() );
24086
24087 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24088 return;
24089 }
24090
24091 if (
24092 $add_upgrade_link &&
24093 $this->is_pricing_page_visible() &&
24094 $this->is_submenu_item_visible( 'pricing' )
24095 ) {
24096 $this->add_plugin_action_link(
24097 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24098 $this->get_upgrade_url(),
24099 false,
24100 7,
24101 'upgrade'
24102 );
24103 }
24104
24105 if (
24106 $add_addons_link &&
24107 $this->has_addons() &&
24108 $this->is_submenu_item_visible( 'addons' )
24109 ) {
24110 $this->add_plugin_action_link(
24111 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24112 $this->_get_admin_page_url( 'addons' ),
24113 false,
24114 9,
24115 'addons'
24116 );
24117 }
24118 }
24119
24120 /**
24121 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24122 *
24123 * @author Leo Fajardo (@leorw)
24124 * @since 1.1.9
24125 */
24126 function _add_license_action_link() {
24127 $this->_logger->entrance();
24128
24129 if ( ! self::is_ajax() ) {
24130 // Inject license activation dialog UI and client side code.
24131 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24132 }
24133
24134 $link_text = $this->is_free_plan() ?
24135 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24136 $this->get_text_inline( 'Change License', 'change-license' );
24137
24138 $this->add_plugin_action_link(
24139 $link_text,
24140 '#',
24141 false,
24142 11,
24143 ( 'activate-license ' . $this->get_unique_affix() )
24144 );
24145 }
24146
24147 /**
24148 * @author Leo Fajardo (@leorw)
24149 * @since 2.0.2
24150 */
24151 function _add_premium_version_upgrade_selection_action() {
24152 $this->_logger->entrance();
24153
24154 if ( ! self::is_ajax() ) {
24155 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24156 }
24157 }
24158
24159 /**
24160 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24161 *
24162 * @author Leo Fajardo (@leorw)
24163 * @since 1.2.1.5
24164 */
24165 function _add_tracking_links() {
24166 if ( ! current_user_can( 'manage_options' ) ) {
24167 return;
24168 }
24169
24170 $this->_logger->entrance();
24171
24172 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24173 // 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.
24174 return;
24175 }
24176
24177 if (
24178 $this->is_addon() &&
24179 ! $this->is_only_premium()
24180 ) {
24181 $parent = $this->get_parent_instance();
24182
24183 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24184 return;
24185 }
24186 }
24187
24188 if ( fs_is_network_admin() ) {
24189 if ( ! $this->_is_network_active ) {
24190 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24191 return;
24192 } else if ( $this->is_network_delegated_connection() ) {
24193 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24194 return;
24195 }
24196 } else {
24197 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24198 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24199 return;
24200 }
24201 }
24202
24203 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24204 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24205 $this->connect_again();
24206
24207 return;
24208 }
24209 }
24210
24211 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24212 ( $this->is_theme() && ! self::is_themes_page() )
24213 ) {
24214 // Only show tracking links on the plugins and themes pages.
24215 return;
24216 }
24217
24218 if (
24219 $this->is_activation_mode() &&
24220 $this->is_premium() &&
24221 ! $this->is_registered()
24222 ) {
24223 // If not yet registered and running the premium code base, a license activation link will already be shown.
24224 return;
24225 }
24226
24227 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24228 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24229 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24230 return;
24231 }
24232 }
24233
24234 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24235 return;
24236 }
24237
24238 $link_text_id = '';
24239 $url = '#';
24240
24241 if ( $this->is_registered( true ) ) {
24242 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24243 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24244 } else {
24245 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24246 }
24247 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24248 /**
24249 * Show opt-in link only if skipped or in activation mode.
24250 */
24251 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24252
24253 $params = ! $this->is_anonymous() ?
24254 array() :
24255 array(
24256 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24257 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24258 );
24259
24260 $url = $this->get_activation_url( $params );
24261 }
24262
24263 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24264
24265 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24266 $this->add_plugin_action_link(
24267 $link_text_id,
24268 $url,
24269 false,
24270 13,
24271 "opt-in-or-opt-out {$this->_slug}"
24272 );
24273 }
24274 }
24275
24276 /**
24277 * Get the URL of the page that should be loaded right after the plugin activation.
24278 *
24279 * @author Vova Feldman (@svovaf)
24280 * @since 1.1.7.4
24281 *
24282 * @return string
24283 */
24284 function get_after_plugin_activation_redirect_url() {
24285 $url = false;
24286
24287 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24288 $first_time_path = $this->_menu->get_first_time_path(
24289 fs_is_network_admin() && $this->_is_network_active
24290 );
24291
24292 if ( $this->is_activation_mode() ) {
24293 $url = $this->get_activation_url();
24294 } else if ( ! empty( $first_time_path ) ) {
24295 $url = $first_time_path;
24296 } else {
24297 $page = '';
24298 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24299 if ( $this->is_network_registered() ) {
24300 $page = 'account';
24301 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24302 $this->maybe_set_slug_and_network_menu_exists_flag();
24303 }
24304 }
24305
24306 $url = $this->_get_admin_page_url( $page );
24307 }
24308 } else {
24309 $plugin_fs = false;
24310
24311 if ( $this->is_parent_plugin_installed() ) {
24312 $plugin_fs = self::get_parent_instance();
24313 }
24314
24315 if ( is_object( $plugin_fs ) ) {
24316 if ( ! $plugin_fs->is_registered() ) {
24317 // Forward to parent plugin connect when parent not registered.
24318 $url = $plugin_fs->get_activation_url();
24319 } else {
24320 // Forward to account page.
24321 $url = $plugin_fs->_get_admin_page_url( 'account' );
24322 }
24323 }
24324 }
24325
24326 return $url;
24327 }
24328
24329 /**
24330 * Forward page to activation page.
24331 *
24332 * @author Vova Feldman (@svovaf)
24333 * @since 1.0.3
24334 */
24335 function _redirect_on_activation_hook() {
24336 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24337 $url = $this->get_after_plugin_activation_redirect_url();
24338
24339 if ( is_string( $url ) ) {
24340 fs_redirect( $url );
24341 }
24342 }
24343 }
24344
24345 /**
24346 * Modify plugin's page action links collection.
24347 *
24348 * @author Vova Feldman (@svovaf)
24349 * @since 1.0.0
24350 *
24351 * @param array $links
24352 * @param $file
24353 *
24354 * @return array
24355 */
24356 function _modify_plugin_action_links_hook( $links, $file ) {
24357 $this->_logger->entrance();
24358
24359 $passed_deactivate = false;
24360 $deactivate_link = '';
24361 $before_deactivate = array();
24362 $after_deactivate = array();
24363 foreach ( $links as $key => $link ) {
24364 if ( 'deactivate' === $key ) {
24365 $deactivate_link = $link;
24366 $passed_deactivate = true;
24367 continue;
24368 }
24369
24370 if ( ! $passed_deactivate ) {
24371 $before_deactivate[ $key ] = $link;
24372 } else {
24373 $after_deactivate[ $key ] = $link;
24374 }
24375 }
24376
24377 ksort( $this->_action_links );
24378
24379 foreach ( $this->_action_links as $new_links ) {
24380 foreach ( $new_links as $link ) {
24381 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24382 }
24383 }
24384
24385 if ( ! empty( $deactivate_link ) ) {
24386 /**
24387 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24388 *
24389 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24390 */
24391 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24392
24393 // Append deactivation link.
24394 $before_deactivate['deactivate'] = $deactivate_link;
24395 }
24396
24397 return array_merge( $before_deactivate, $after_deactivate );
24398 }
24399
24400 /**
24401 * Adds admin message.
24402 *
24403 * @author Vova Feldman (@svovaf)
24404 * @since 1.0.4
24405 *
24406 * @param string $message
24407 * @param string $title
24408 * @param string $type
24409 */
24410 function add_admin_message( $message, $title = '', $type = 'success' ) {
24411 $this->_admin_notices->add( $message, $title, $type );
24412 }
24413
24414 /**
24415 * Adds sticky admin message.
24416 *
24417 * @author Vova Feldman (@svovaf)
24418 * @since 1.1.0
24419 *
24420 * @param string $message
24421 * @param string $id
24422 * @param string $title
24423 * @param string $type
24424 */
24425 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24426 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24427 }
24428
24429 /**
24430 * Check if the paid version of the module is installed.
24431 *
24432 * @author Vova Feldman (@svovaf)
24433 * @since 2.2.0
24434 *
24435 * @return bool
24436 */
24437 private function is_premium_version_installed() {
24438 $premium_plugin_basename = $this->premium_plugin_basename();
24439
24440 if ( $this->is_theme() ) {
24441 return $this->can_activate_theme( $this->get_premium_slug() );
24442 }
24443
24444 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24445 }
24446
24447 /**
24448 * Helper function that returns the final steps for the upgrade completion.
24449 *
24450 * If the module is already running the premium code, returns an empty string.
24451 *
24452 * @author Vova Feldman (@svovaf)
24453 * @since 1.2.1
24454 *
24455 * @param string $plan_title
24456 *
24457 * @return string
24458 */
24459 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24460 $this->_logger->entrance();
24461
24462 $activate_license_string = $this->get_license_network_activation_notice();
24463
24464 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24465 return '' . $activate_license_string;
24466 }
24467
24468 if ( empty( $plan_title ) ) {
24469 $plan_title = $this->get_plan_title();
24470 }
24471
24472 if ( $this->is_premium_version_installed() ) {
24473 /**
24474 * If the premium version is already installed, instead of showing the installation instructions,
24475 * tell the current user to activate it.
24476 *
24477 * @author Leo Fajardo (@leorw)
24478 * @since 2.2.1
24479 */
24480 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24481 $this->get_premium_slug() :
24482 $this->premium_plugin_basename();
24483
24484 return sprintf(
24485 /* translators: %1$s: Product title; %2$s: Plan title */
24486 $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' ),
24487 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24488 $plan_title,
24489 sprintf(
24490 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24491 ( $this->is_theme() ?
24492 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24493 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24494 esc_html( sprintf(
24495 /* translators: %s: Plan title */
24496 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24497 $plan_title
24498 ) )
24499 )
24500 );
24501 } else {
24502 // @since 1.2.1.5 The free version is auto deactivated.
24503 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24504 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24505 '';
24506
24507 return sprintf(
24508 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24509 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24510 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24511 $this->get_latest_download_link( sprintf(
24512 /* translators: %s: Plan title */
24513 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24514 $plan_title
24515 ) ),
24516 $deactivation_step,
24517 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24518 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24519 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24520 );
24521 }
24522 }
24523
24524 /**
24525 * @author Leo Fajardo (@leorw)
24526 * @since 2.5.3
24527 *
24528 * @param string $message_before_the_instructions
24529 * @param string $message_id
24530 * @param string $plan_title
24531 */
24532 private function add_complete_upgrade_instructions_notice(
24533 $message_before_the_instructions,
24534 $message_id,
24535 $plan_title = ''
24536 ) {
24537 $this->_admin_notices->add_sticky(
24538 $message_before_the_instructions .
24539 $this->get_complete_upgrade_instructions( $plan_title ),
24540 $message_id,
24541 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24542 );
24543 }
24544
24545 /**
24546 * @author Leo Fajardo (@leorw)
24547 * @since 2.5.3
24548 *
24549 * @param bool $is_upgrade
24550 */
24551 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24552 $this->add_complete_upgrade_instructions_notice(
24553 $is_upgrade ?
24554 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24555 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24556 'plan_upgraded'
24557 );
24558 }
24559
24560 /**
24561 * @author Leo Fajardo (@leorw)
24562 * @since 2.1.0
24563 *
24564 * @param string $url
24565 * @param array $request
24566 * @param int $success_cache_expiration
24567 * @param int $failure_cache_expiration
24568 * @param bool $maybe_enrich_request_for_debug
24569 *
24570 * @return WP_Error|array
24571 */
24572 static function safe_remote_post(
24573 &$url,
24574 $request,
24575 $success_cache_expiration = 0,
24576 $failure_cache_expiration = 0,
24577 $maybe_enrich_request_for_debug = true
24578 ) {
24579 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24580
24581 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24582
24583 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24584 get_transient( $cache_key ) :
24585 false;
24586
24587 if ( false === $response ) {
24588 if ( $maybe_enrich_request_for_debug ) {
24589 FS_DebugManager::enrich_request_for_debug( $url, $request );
24590 }
24591
24592 if ( ! isset( $request['method'] ) ) {
24593 $request['method'] = 'POST';
24594 }
24595
24596 $response = FS_Api::remote_request( $url, $request );
24597
24598 if (
24599 'https://' === substr( $url, 0, 8 ) &&
24600 FS_Api::is_ssl_error_response( $response )
24601 ) {
24602 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24603 $url = 'http://' . substr( $url, 8 );
24604
24605 $request['timeout'] = 15;
24606 $response = FS_Api::remote_request( $url, $request );
24607 }
24608
24609 if ( false !== $cache_key ) {
24610 set_transient(
24611 $cache_key,
24612 $response,
24613 ( ( $response instanceof WP_Error ) ?
24614 $failure_cache_expiration :
24615 $success_cache_expiration )
24616 );
24617 }
24618 }
24619
24620 return $response;
24621 }
24622
24623 /**
24624 * This method is used to enrich the after upgrade notice instructions when the upgraded
24625 * license cannot be activated network wide (license quota isn't large enough).
24626 *
24627 * @author Vova Feldman (@svovaf)
24628 * @since 2.0.0
24629 *
24630 * @return string
24631 */
24632 private function get_license_network_activation_notice() {
24633 if ( ! $this->_is_network_active ) {
24634 // Module isn't network level activated.
24635 return '';
24636 }
24637
24638 if ( ! fs_is_network_admin() ) {
24639 // Not network level admin.
24640 return '';
24641 }
24642
24643 if ( get_blog_count() == 1 ) {
24644 // There's only a single site in the network so if there's a context license it was already activated.
24645 return '';
24646 }
24647
24648 if ( ! is_object( $this->_license ) ) {
24649 // No context license.
24650 return '';
24651 }
24652
24653 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24654 // 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).
24655 return '';
24656 }
24657
24658 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24659 // 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).
24660 return '';
24661 }
24662
24663 return sprintf(
24664 $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' ),
24665 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24666 '</a>'
24667 );
24668 }
24669
24670 /**
24671 * @author Vova Feldman (@svovaf)
24672 * @since 1.2.1.7
24673 *
24674 * @param string $key
24675 *
24676 * @return string
24677 */
24678 function get_text( $key ) {
24679 return fs_text( $key, $this->_slug );
24680 }
24681
24682 /**
24683 * @author Vova Feldman (@svovaf)
24684 * @since 1.2.3
24685 *
24686 * @param string $text Translatable string.
24687 * @param string $key String key for overrides.
24688 *
24689 * @return string
24690 */
24691 function get_text_inline( $text, $key = '' ) {
24692 return _fs_text_inline( $text, $key, $this->_slug );
24693 }
24694
24695 /**
24696 * @author Vova Feldman (@svovaf)
24697 * @since 1.2.3
24698 *
24699 * @param string $text Translatable string.
24700 * @param string $context Context information for the translators.
24701 * @param string $key String key for overrides.
24702 *
24703 * @return string
24704 */
24705 function get_text_x_inline( $text, $context, $key ) {
24706 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24707 }
24708
24709 /**
24710 * @author Vova Feldman (@svovaf)
24711 * @since 1.2.3
24712 *
24713 * @param string $text Translatable string.
24714 * @param string $key String key for overrides.
24715 *
24716 * @return string
24717 */
24718 function esc_html_inline( $text, $key ) {
24719 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
24720 }
24721
24722 #----------------------------------------------------------------------------------
24723 #region Versioning
24724 #----------------------------------------------------------------------------------
24725
24726 /**
24727 * Check if Freemius in SDK upgrade mode.
24728 *
24729 * @author Vova Feldman (@svovaf)
24730 * @since 1.0.9
24731 *
24732 * @return bool
24733 */
24734 function is_sdk_upgrade_mode() {
24735 return isset( $this->_storage->sdk_upgrade_mode ) ?
24736 $this->_storage->sdk_upgrade_mode :
24737 false;
24738 }
24739
24740 /**
24741 * Turn SDK upgrade mode off.
24742 *
24743 * @author Vova Feldman (@svovaf)
24744 * @since 1.0.9
24745 */
24746 function set_sdk_upgrade_complete() {
24747 $this->_storage->sdk_upgrade_mode = false;
24748 }
24749
24750 /**
24751 * Check if plugin upgrade mode.
24752 *
24753 * @author Vova Feldman (@svovaf)
24754 * @since 1.0.9
24755 *
24756 * @return bool
24757 */
24758 function is_plugin_upgrade_mode() {
24759 return isset( $this->_storage->plugin_upgrade_mode ) ?
24760 $this->_storage->plugin_upgrade_mode :
24761 false;
24762 }
24763
24764 /**
24765 * Turn plugin upgrade mode off.
24766 *
24767 * @author Vova Feldman (@svovaf)
24768 * @since 1.0.9
24769 */
24770 function set_plugin_upgrade_complete() {
24771 $this->_storage->plugin_upgrade_mode = false;
24772
24773 $license_migration = ! empty( $this->_storage->license_migration ) ?
24774 $this->_storage->license_migration :
24775 array();
24776
24777 $license_migration['is_migrating'] = false;
24778
24779 $this->_storage->license_migration = $license_migration;
24780 }
24781
24782 #endregion
24783
24784 #----------------------------------------------------------------------------------
24785 #region Permissions
24786 #----------------------------------------------------------------------------------
24787
24788 /**
24789 * Check if specific permission requested.
24790 *
24791 * @author Vova Feldman (@svovaf)
24792 * @since 1.1.6
24793 *
24794 * @param string $permission
24795 *
24796 * @return bool
24797 */
24798 function is_permission_requested( $permission ) {
24799 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
24800 }
24801
24802 #endregion
24803
24804 #----------------------------------------------------------------------------------
24805 #region Auto Activation
24806 #----------------------------------------------------------------------------------
24807
24808 /**
24809 * Hints the SDK if running an auto-installation.
24810 *
24811 * @var bool
24812 */
24813 private $_isAutoInstall = false;
24814
24815 /**
24816 * After upgrade callback to install and auto activate a plugin.
24817 * This code will only be executed on explicit request from the user,
24818 * following the practice Jetpack are using with their theme installations.
24819 *
24820 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
24821 *
24822 * @author Vova Feldman (@svovaf)
24823 * @since 1.2.1.7
24824 */
24825 function _install_premium_version_ajax_action() {
24826 $this->_logger->entrance();
24827
24828 $this->check_ajax_referer( 'install_premium_version' );
24829
24830 if ( ! $this->is_registered() ) {
24831 // Not registered.
24832 self::shoot_ajax_failure( array(
24833 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
24834 'code' => 'premium_installed',
24835 ) );
24836 }
24837
24838 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
24839
24840 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24841 // Invalid ID.
24842 self::shoot_ajax_failure( array(
24843 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24844 'code' => 'invalid_module_id',
24845 ) );
24846 }
24847
24848 if ( $plugin_id == $this->get_id() ) {
24849 if ( $this->is_premium() ) {
24850 // Already using the premium code version.
24851 self::shoot_ajax_failure( array(
24852 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
24853 'code' => 'premium_installed',
24854 ) );
24855 }
24856 if ( ! $this->can_use_premium_code() ) {
24857 // Don't have access to the premium code.
24858 self::shoot_ajax_failure( array(
24859 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
24860 'code' => 'invalid_license',
24861 ) );
24862 }
24863 if ( ! $this->has_release_on_freemius() ) {
24864 // Plugin is a serviceware, no premium code version.
24865 self::shoot_ajax_failure( array(
24866 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
24867 'code' => 'premium_version_missing',
24868 ) );
24869 }
24870 } else {
24871 $addon = $this->get_addon( $plugin_id );
24872
24873 if ( ! is_object( $addon ) ) {
24874 // Invalid add-on ID.
24875 self::shoot_ajax_failure( array(
24876 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
24877 'code' => 'invalid_module_id',
24878 ) );
24879 }
24880
24881 if ( $this->is_addon_activated( $plugin_id, true ) ) {
24882 // Premium add-on version is already activated.
24883 self::shoot_ajax_failure( array(
24884 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
24885 'code' => 'premium_installed',
24886 ) );
24887 }
24888 }
24889
24890 $this->_isAutoInstall = true;
24891
24892 // Try to install and activate.
24893 $updater = FS_Plugin_Updater::instance( $this );
24894 $result = $updater->install_and_activate_plugin( $plugin_id );
24895
24896 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
24897 self::shoot_ajax_failure( array(
24898 'message' => $result['message'],
24899 'code' => $result['code'],
24900 ) );
24901 }
24902
24903 self::shoot_ajax_success( $result );
24904 }
24905
24906 /**
24907 * Displays module activation dialog box after a successful upgrade
24908 * where the user explicitly requested to auto download and install
24909 * the premium version.
24910 *
24911 * @author Vova Feldman (@svovaf)
24912 * @since 1.2.1.7
24913 */
24914 function _add_auto_installation_dialog_box() {
24915 $this->_logger->entrance();
24916
24917 if ( ! $this->is_registered() ) {
24918 // Not registered.
24919 return;
24920 }
24921
24922 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
24923
24924 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
24925 // Invalid module ID.
24926 return;
24927 }
24928
24929 if ( $plugin_id == $this->get_id() ) {
24930 if ( $this->is_premium() ) {
24931 // Already using the premium code version.
24932 return;
24933 }
24934 if ( ! $this->can_use_premium_code() ) {
24935 // Don't have access to the premium code.
24936 return;
24937 }
24938 if ( ! $this->has_release_on_freemius() ) {
24939 // Plugin is a serviceware, no premium code version.
24940 return;
24941 }
24942 } else {
24943 $addon = $this->get_addon( $plugin_id );
24944
24945 if ( ! is_object( $addon ) ) {
24946 // Invalid add-on ID.
24947 return;
24948 }
24949
24950 if ( $this->is_addon_activated( $plugin_id, true ) ) {
24951 // Premium add-on version is already activated.
24952 return;
24953 }
24954 }
24955
24956 $vars = array(
24957 'id' => $this->_module_id,
24958 'target_module_id' => $plugin_id,
24959 'slug' => $this->_slug,
24960 );
24961
24962 fs_require_template( 'auto-installation.php', $vars );
24963 }
24964
24965 #endregion
24966
24967 #--------------------------------------------------------------------------------
24968 #region Tabs Integration
24969 #--------------------------------------------------------------------------------
24970
24971 #region Module's Original Tabs
24972
24973 /**
24974 * Inject a JavaScript logic to capture the theme tabs HTML.
24975 *
24976 * @author Vova Feldman (@svovaf)
24977 * @since 1.2.2.7
24978 */
24979 function _tabs_capture() {
24980 $this->_logger->entrance();
24981
24982 if (
24983 ! $this->is_product_settings_page() ||
24984 ! $this->should_page_include_tabs() ||
24985 ! $this->is_matching_url( $this->main_menu_url() )
24986 ) {
24987 return;
24988 }
24989
24990 $params = array(
24991 'id' => $this->_module_id,
24992 );
24993
24994 fs_require_once_template( 'tabs-capture-js.php', $params );
24995 }
24996
24997 /**
24998 * Cache theme's tabs HTML for a week. The cache will also be set as expired
24999 * after version and type (free/premium) changes, in addition to the week period.
25000 *
25001 * @author Vova Feldman (@svovaf)
25002 * @since 1.2.2.7
25003 */
25004 function _store_tabs_ajax_action() {
25005 $this->_logger->entrance();
25006
25007 $this->check_ajax_referer( 'store_tabs' );
25008
25009 // Init filesystem if not yet initiated.
25010 WP_Filesystem();
25011
25012 // Get POST body HTML data.
25013 global $wp_filesystem;
25014 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25015
25016 if ( is_string( $tabs_html ) ) {
25017 $tabs_html = trim( $tabs_html );
25018 }
25019
25020 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25021 self::shoot_ajax_failure();
25022 }
25023
25024 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25025
25026 self::shoot_ajax_success();
25027 }
25028
25029 /**
25030 * Cache theme's settings page custom styles. The cache will also be set as expired
25031 * after version and type (free/premium) changes, in addition to the week period.
25032 *
25033 * @author Vova Feldman (@svovaf)
25034 * @since 1.2.2.7
25035 */
25036 function _store_tabs_styles() {
25037 $this->_logger->entrance();
25038
25039 if (
25040 ! $this->is_product_settings_page() ||
25041 ! $this->should_page_include_tabs() ||
25042 ! $this->is_matching_url( $this->main_menu_url() )
25043 ) {
25044 return;
25045 }
25046
25047 $wp_styles = wp_styles();
25048
25049 $theme_styles_url = get_template_directory_uri();
25050
25051 $stylesheets = array();
25052 foreach ( $wp_styles->queue as $handler ) {
25053 if ( fs_starts_with( $handler, 'fs_' ) ) {
25054 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25055 continue;
25056 }
25057
25058 /**
25059 * @var _WP_Dependency $stylesheet
25060 */
25061 $stylesheet = $wp_styles->registered[ $handler ];
25062
25063 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25064 $stylesheets[] = $stylesheet->src;
25065 }
25066 }
25067
25068 if ( ! empty( $stylesheets ) ) {
25069 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25070 }
25071 }
25072
25073 /**
25074 * Check if module's original settings page has any tabs.
25075 *
25076 * @author Vova Feldman (@svovaf)
25077 * @since 1.2.2.7
25078 *
25079 * @return bool
25080 */
25081 private function has_tabs() {
25082 return $this->_cache->has( 'tabs' );
25083 }
25084
25085 /**
25086 * Get module's settings page HTML content, starting
25087 * from the beginning of the <div class="wrap"> element,
25088 * until the tabs HTML (including).
25089 *
25090 * @author Vova Feldman (@svovaf)
25091 * @since 1.2.2.7
25092 *
25093 * @return string
25094 */
25095 private function get_tabs_html() {
25096 $this->_logger->entrance();
25097
25098 return $this->_cache->get( 'tabs' );
25099 }
25100
25101 /**
25102 * Check if page should include tabs.
25103 *
25104 * @author Vova Feldman (@svovaf)
25105 * @since 1.2.2.7
25106 *
25107 * @return bool
25108 */
25109 private function should_page_include_tabs() {
25110 if ( ! $this->has_settings_menu() ) {
25111 // Don't add tabs if no settings at all.
25112 return false;
25113 }
25114
25115 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25116 // Only add tabs to themes for now.
25117 return false;
25118 }
25119
25120 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25121 // Only add tabs to monetizing themes.
25122 return false;
25123 }
25124
25125 if ( ! $this->is_product_settings_page() ) {
25126 // Only add tabs if browsing one of the product's setting pages.
25127 return false;
25128 }
25129
25130 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25131 // Don't include tabs in the activation page.
25132 return false;
25133 }
25134
25135 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25136 // Don't add tabs on checkout page, we want to reduce distractions
25137 // as much as possible.
25138 return false;
25139 }
25140
25141 return true;
25142 }
25143
25144 /**
25145 * Add the tabs HTML before the setting's page content and
25146 * enqueue any required stylesheets.
25147 *
25148 * @author Vova Feldman (@svovaf)
25149 * @since 1.2.2.7
25150 *
25151 * @return bool If tabs were included.
25152 */
25153 function _add_tabs_before_content() {
25154 $this->_logger->entrance();
25155
25156 if ( ! $this->should_page_include_tabs() ) {
25157 return false;
25158 }
25159
25160 $tabs_html = $this->get_tabs_html();
25161
25162 if ( empty( $tabs_html ) ) {
25163 return false;
25164 }
25165
25166 /**
25167 * Enqueue the original stylesheets that are included in the
25168 * theme settings page. That way, if the theme settings has
25169 * some custom _styled_ content above the tabs UI, this
25170 * will make sure that the styling is preserved.
25171 */
25172 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25173 if ( is_array( $stylesheets ) ) {
25174 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25175 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25176 }
25177 }
25178
25179 // Cut closing </div> tag.
25180 echo substr( trim( $tabs_html ), 0, - 6 );
25181
25182 return true;
25183 }
25184
25185 /**
25186 * Add the tabs closing HTML after the setting's page content.
25187 *
25188 * @author Vova Feldman (@svovaf)
25189 * @since 1.2.2.7
25190 *
25191 * @return bool If tabs closing HTML was included.
25192 */
25193 function _add_tabs_after_content() {
25194 $this->_logger->entrance();
25195
25196 if ( ! $this->should_page_include_tabs() ) {
25197 return false;
25198 }
25199
25200 echo '</div>';
25201
25202 return true;
25203 }
25204
25205 #endregion
25206
25207 /**
25208 * Add in-page JavaScript to inject the Freemius tabs into
25209 * the module's setting tabs section.
25210 *
25211 * @author Vova Feldman (@svovaf)
25212 * @since 1.2.2.7
25213 */
25214 function _add_freemius_tabs() {
25215 $this->_logger->entrance();
25216
25217 if ( ! $this->should_page_include_tabs() ) {
25218 return;
25219 }
25220
25221 $params = array( 'id' => $this->_module_id );
25222 fs_require_once_template( 'tabs.php', $params );
25223 }
25224
25225 #endregion
25226
25227 #--------------------------------------------------------------------------------
25228 #region Customizer Integration for Themes
25229 #--------------------------------------------------------------------------------
25230
25231 /**
25232 * @author Vova Feldman (@svovaf)
25233 * @since 1.2.2.7
25234 *
25235 * @param WP_Customize_Manager $customizer
25236 */
25237 function _customizer_register( $customizer ) {
25238 $this->_logger->entrance();
25239
25240 if ( $this->is_pricing_page_visible() ) {
25241 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25242
25243 $customizer->add_section( 'freemius_upsell', array(
25244 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25245 'priority' => 1,
25246 ) );
25247 $customizer->add_setting( 'freemius_upsell', array(
25248 'sanitize_callback' => 'esc_html',
25249 ) );
25250
25251 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25252 'fs' => $this,
25253 'section' => 'freemius_upsell',
25254 'priority' => 100,
25255 ) ) );
25256 }
25257
25258 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25259 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25260
25261 // Main Documentation Link In Customizer Root.
25262 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25263 'fs' => $this,
25264 'priority' => 1000,
25265 ) ) );
25266 }
25267 }
25268
25269 #endregion
25270
25271 /**
25272 * If the theme has a paid version, add some custom
25273 * styling to the theme's premium version (if exists)
25274 * to highlight that it's the premium version of the
25275 * same theme, making it easier for identification
25276 * after the user upgrades and upload it to the site.
25277 *
25278 * @author Vova Feldman (@svovaf)
25279 * @since 1.2.2.7
25280 */
25281 function _style_premium_theme() {
25282 $this->_logger->entrance();
25283
25284 if ( ! self::is_themes_page() ) {
25285 // Only include in the themes page.
25286 return;
25287 }
25288
25289 if ( ! $this->has_paid_plan() ) {
25290 // Only include if has any paid plans.
25291 return;
25292 }
25293
25294 $params = null;
25295 fs_require_once_template( '/js/jquery.content-change.php', $params );
25296
25297 $params = array(
25298 'slug' => $this->_slug,
25299 'id' => $this->_module_id,
25300 );
25301
25302 fs_require_template( '/js/style-premium-theme.php', $params );
25303 }
25304
25305 /**
25306 * This method will return the absolute URL of the module's local icon.
25307 *
25308 * When you are running your plugin or theme on a **localhost** environment, if the icon
25309 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25310 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25311 * If an icon is found, this method will automatically attempt to download the icon and store it
25312 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25313 *
25314 * It's important to mention that this method is NOT phoning home since the developer will deploy
25315 * the product with the local icon in the assets folder. The download process just simplifies
25316 * the process for the developer.
25317 *
25318 * @author Vova Feldman (@svovaf)
25319 * @since 2.0.0
25320 *
25321 * @return string
25322 */
25323 function get_local_icon_url() {
25324 global $fs_active_plugins;
25325
25326 /**
25327 * @since 1.1.7.5
25328 */
25329 $local_path = $this->apply_filters( 'plugin_icon', false );
25330
25331 if ( is_string( $local_path ) ) {
25332 $icons = array( $local_path );
25333 } else {
25334 $img_dir = WP_FS__DIR_IMG;
25335
25336 // Locate the main assets folder.
25337 if ( 1 < count( $fs_active_plugins->plugins ) ) {
25338 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25339
25340 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25341 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25342 $img_dir = $plugin_or_theme_img_dir
25343 . '/'
25344 /**
25345 * The basename will be `themes` or the basename of a custom themes directory.
25346 *
25347 * @author Leo Fajardo (@leorw)
25348 * @since 2.2.3
25349 */
25350 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25351 . '/assets/img';
25352
25353 break;
25354 }
25355 }
25356 }
25357
25358 // Try to locate the icon in the assets folder.
25359 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25360
25361 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25362 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25363 $icons = array(
25364 fs_normalize_path( $img_dir . '/theme-icon.png' )
25365 );
25366 } else {
25367 $icon_found = false;
25368 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25369
25370 if ( ! function_exists( 'get_filesystem_method' ) ) {
25371 require_once ABSPATH . 'wp-admin/includes/file.php';
25372 }
25373
25374 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25375
25376 /**
25377 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25378 *
25379 * This code will only be executed once during the testing
25380 * of the plugin in a local environment. The plugin icon file WILL
25381 * already exist in the assets folder when the plugin is deployed to
25382 * the repository.
25383 */
25384 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25385 // Fetch icon from Freemius.
25386 $icon = $this->fetch_remote_icon_url();
25387
25388 // Fetch icon from WordPress.org.
25389 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25390 if ( ! function_exists( 'plugins_api' ) ) {
25391 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25392 }
25393
25394 $plugin_information = plugins_api( 'plugin_information', array(
25395 'slug' => $this->_slug,
25396 'fields' => array(
25397 'sections' => false,
25398 'tags' => false,
25399 'icons' => true
25400 )
25401 ) );
25402
25403 if (
25404 ! is_wp_error( $plugin_information )
25405 && isset( $plugin_information->icons )
25406 && ! empty( $plugin_information->icons )
25407 ) {
25408 /**
25409 * Get the smallest icon.
25410 *
25411 * @author Leo Fajardo (@leorw)
25412 * @since 1.2.2
25413 */
25414 $icon = end( $plugin_information->icons );
25415 }
25416 }
25417
25418 if ( ! empty( $icon ) ) {
25419 if ( 0 !== strpos( $icon, 'http' ) ) {
25420 $icon = 'http:' . $icon;
25421 }
25422
25423 /**
25424 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25425 *
25426 * @author Leo Fajardo (@leorw)
25427 * @since 1.2.2
25428 */
25429 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25430
25431 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25432
25433 // Try to download the icon.
25434 $icon_found = fs_download_image( $icon, $local_path );
25435 }
25436 }
25437
25438 if ( ! $icon_found ) {
25439 // No icons found, fallback to default icon.
25440 if ( $have_write_permissions ) {
25441 // If have write permissions, copy default icon.
25442 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25443 } else {
25444 // If doesn't have write permissions, use default icon path.
25445 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25446 }
25447 }
25448
25449 $icons = array( $local_path );
25450 }
25451 }
25452 }
25453
25454 $icon_dir = dirname( $icons[0] );
25455
25456 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25457 }
25458
25459 /**
25460 * Fetch module's extended info.
25461 *
25462 * @author Vova Feldman (@svovaf)
25463 * @since 2.0.0
25464 *
25465 * @return object|mixed
25466 */
25467 private function fetch_module_info() {
25468 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25469 }
25470
25471 /**
25472 * Fetch module's remote icon URL.
25473 *
25474 * @author Vova Feldman (@svovaf)
25475 * @since 2.0.0
25476 *
25477 * @return string
25478 */
25479 function fetch_remote_icon_url() {
25480 $info = $this->fetch_module_info();
25481
25482 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25483 $info->icon :
25484 '';
25485 }
25486
25487 #--------------------------------------------------------------------------------
25488 #region GDPR
25489 #--------------------------------------------------------------------------------
25490
25491 /**
25492 * @author Leo Fajardo (@leorw)
25493 * @since 2.1.0
25494 *
25495 * @param array $user_plugins
25496 *
25497 * @return string
25498 */
25499 private function get_gdpr_admin_notice_string( $user_plugins ) {
25500 $this->_logger->entrance();
25501
25502 $addons = self::get_all_addons();
25503
25504 foreach ( $user_plugins as $user_plugin ) {
25505 $has_addons = isset( $addons[ $user_plugin->id ] );
25506
25507 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25508 if ( $this->_module_id == $user_plugin->id ) {
25509 $addons = $this->get_addons();
25510 $has_addons = ( ! empty( $addons ) );
25511 } else {
25512 $plugin_api = FS_Api::instance(
25513 $user_plugin->id,
25514 'plugin',
25515 $user_plugin->id,
25516 $user_plugin->public_key,
25517 ! $user_plugin->is_live,
25518 false,
25519 $this->get_sdk_version()
25520 );
25521
25522 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25523
25524 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25525 is_array( $addons_result->plugins ) &&
25526 ! empty( $addons_result->plugins )
25527 ) {
25528 $has_addons = true;
25529 }
25530 }
25531 }
25532
25533 $user_plugin->has_addons = $has_addons;
25534 }
25535
25536 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25537
25538 $multiple_products_text = '';
25539
25540 if ( $is_single_parent_product ) {
25541 $single_parent_product = reset( $user_plugins );
25542
25543 $thank_you = sprintf(
25544 "<span data-plugin-id='%d'>%s</span>",
25545 $single_parent_product->id,
25546 sprintf(
25547 $single_parent_product->has_addons ?
25548 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25549 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25550 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25551 )
25552 );
25553
25554 $already_opted_in = sprintf(
25555 $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' ),
25556 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25557 );
25558 } else {
25559 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25560 $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' );
25561
25562 $products_and_add_ons = '';
25563 foreach ( $user_plugins as $user_plugin ) {
25564 if ( ! empty( $products_and_add_ons ) ) {
25565 $products_and_add_ons .= ', ';
25566 }
25567
25568 if ( ! $user_plugin->has_addons ) {
25569 $products_and_add_ons .= sprintf(
25570 "<span data-plugin-id='%d'>%s</span>",
25571 $user_plugin->id,
25572 $user_plugin->title
25573 );
25574 } else {
25575 $products_and_add_ons .= sprintf(
25576 "<span data-plugin-id='%d'>%s</span>",
25577 $user_plugin->id,
25578 sprintf(
25579 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25580 $user_plugin->title
25581 )
25582 );
25583 }
25584 }
25585
25586 $multiple_products_text = sprintf(
25587 "<small class='products'><strong>%s:</strong> %s</small>",
25588 $this->get_text_inline( 'Products', 'products' ),
25589 $products_and_add_ons
25590 );
25591 }
25592
25593 $actions = sprintf(
25594 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25595 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25596 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25597 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25598 sprintf(
25599 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25600 '<span class="underlined">',
25601 '</span>'
25602 )
25603 );
25604
25605 return sprintf(
25606 '%s %s %s',
25607 $thank_you,
25608 $already_opted_in,
25609 sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '<a href="https://ec.europa.eu/info/law/law-topic/data-protection_en/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
25610 '<br><br>' .
25611 '<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>' .
25612 $actions .
25613 ( $is_single_parent_product ? '' : $multiple_products_text )
25614 );
25615 }
25616
25617 /**
25618 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25619 * plugins and themes they've opted in to.
25620 *
25621 * @author Leo Fajardo (@leorw)
25622 * @since 2.1.0
25623 *
25624 * @param string $user_email
25625 * @param string $license_key
25626 * @param array $plugin_ids
25627 * @param string|null $license_key
25628 *
25629 * @return array|false
25630 */
25631 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25632 $request = array(
25633 'method' => 'POST',
25634 'body' => array(),
25635 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25636 );
25637
25638 if ( is_string( $user_email ) ) {
25639 $request['body']['email'] = $user_email;
25640 } else {
25641 $request['body']['license_key'] = $license_key;
25642 }
25643
25644 $result = array();
25645
25646 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25647 $total_plugin_ids = count( $plugin_ids );
25648
25649 $plugin_ids_count_per_request = 10;
25650 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25651 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25652
25653 $request['body']['plugin_ids'] = $plugin_ids_set;
25654
25655 $response = self::safe_remote_post(
25656 $url,
25657 $request,
25658 WP_FS__TIME_24_HOURS_IN_SEC,
25659 WP_FS__TIME_12_HOURS_IN_SEC
25660 );
25661
25662 if ( ! is_wp_error( $response ) ) {
25663 $decoded = is_string( $response['body'] ) ?
25664 json_decode( $response['body'] ) :
25665 null;
25666
25667 if (
25668 !is_object($decoded) ||
25669 !isset($decoded->success) ||
25670 true !== $decoded->success ||
25671 !isset( $decoded->data ) ||
25672 !is_array( $decoded->data )
25673 ) {
25674 return false;
25675 }
25676
25677 $result = array_merge( $result, $decoded->data );
25678 }
25679 }
25680
25681 return $result;
25682 }
25683
25684 /**
25685 * @author Leo Fajardo (@leorw)
25686 * @since 2.1.0
25687 */
25688 function _maybe_show_gdpr_admin_notice() {
25689 if ( ! $this->is_user_in_admin() ) {
25690 return;
25691 }
25692
25693 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25694 return;
25695 }
25696
25697 if ( ! $this->is_user_admin() ) {
25698 return;
25699 }
25700
25701 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25702
25703 $lock = FS_User_Lock::instance();
25704
25705 /**
25706 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25707 */
25708 if ( ! $lock->try_lock( 60 ) ) {
25709 return;
25710 }
25711
25712 /**
25713 * @var $current_wp_user WP_User
25714 */
25715 $current_wp_user = self::_get_current_wp_user();
25716
25717 /**
25718 * @var FS_User $current_fs_user
25719 */
25720 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
25721
25722 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
25723
25724 if ( ! is_object( $current_fs_user ) ) {
25725 // 10-year lock.
25726 $lock->lock( $ten_years_in_sec );
25727
25728 return;
25729 }
25730
25731 $gdpr = FS_GDPR_Manager::instance();
25732
25733 if ( $gdpr->is_opt_in_notice_shown() ) {
25734 // 30-day lock.
25735 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25736
25737 return;
25738 }
25739
25740 if ( ! $gdpr->should_show_opt_in_notice() ) {
25741 // 10-year lock.
25742 $lock->lock( $ten_years_in_sec );
25743
25744 return;
25745 }
25746
25747 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
25748 $was_notice_shown_before = ( false !== $last_time_notice_shown );
25749
25750 if ( $was_notice_shown_before &&
25751 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
25752 ) {
25753 // If the notice was shown before, show it again after 30 days from the last time it was shown.
25754 return;
25755 }
25756
25757 /**
25758 * Find all plugin IDs that were installed by the current admin.
25759 */
25760 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
25761
25762 if ( empty( $plugin_ids_map )) {
25763 $lock->lock( $ten_years_in_sec );
25764
25765 return;
25766 }
25767
25768 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25769 $current_fs_user->email,
25770 null,
25771 array_keys( $plugin_ids_map )
25772 );
25773
25774 if ( empty( $user_plugins ) ) {
25775 $lock->lock(
25776 is_array($user_plugins) ?
25777 $ten_years_in_sec :
25778 // Lock for 24-hours on errors.
25779 WP_FS__TIME_24_HOURS_IN_SEC
25780 );
25781
25782 return;
25783 }
25784
25785 $has_unset_marketing_optin = false;
25786
25787 foreach ( $user_plugins as $user_plugin ) {
25788 if ( true == $user_plugin->is_marketing_allowed ) {
25789 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
25790 }
25791
25792 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
25793 $has_unset_marketing_optin = true;
25794 }
25795 }
25796
25797 if ( empty( $plugin_ids_map ) ||
25798 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
25799 ) {
25800 $lock->lock( $ten_years_in_sec );
25801
25802 return;
25803 }
25804
25805 $modules = array_merge(
25806 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25807 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25808 );
25809
25810 foreach ( $modules as $module ) {
25811 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
25812 $plugin_ids_map[ $module->id ] = $module;
25813 }
25814 }
25815
25816 $plugin_title = null;
25817 if ( 1 === count( $plugin_ids_map ) ) {
25818 $module = reset( $plugin_ids_map );
25819 $plugin_title = $module->title;
25820 }
25821
25822 $gdpr->add_opt_in_sticky_notice(
25823 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
25824 $plugin_title
25825 );
25826
25827 $this->add_gdpr_optin_ajax_handler_and_style();
25828
25829 $gdpr->notice_was_just_shown();
25830
25831 // 30-day lock.
25832 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25833 }
25834
25835 /**
25836 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
25837 * marketing.
25838 *
25839 * @author Leo Fajardo (@leorw)
25840 * @since 2.1.0
25841 */
25842 private function disable_opt_in_notice_and_lock_user() {
25843 FS_GDPR_Manager::instance()->disable_opt_in_notice();
25844
25845 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25846
25847 // 10-year lock.
25848 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
25849 }
25850
25851 /**
25852 * @author Leo Fajardo (@leorw)
25853 * @since 2.5.4
25854 */
25855 static function _add_api_connectivity_notice_handler_js() {
25856 fs_require_once_template( 'api-connectivity-message-js.php' );
25857 }
25858
25859 /**
25860 * @author Leo Fajardo (@leorw)
25861 * @since 2.1.0
25862 */
25863 function _add_gdpr_optin_js() {
25864 $vars = array( 'id' => $this->_module_id );
25865
25866 fs_require_once_template( 'gdpr-optin-js.php', $vars );
25867 }
25868
25869 /**
25870 * @author Leo Fajardo (@leorw)
25871 * @since 2.1.0
25872 */
25873 function enqueue_gdpr_optin_notice_style() {
25874 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
25875 }
25876
25877 /**
25878 * @author Leo Fajardo (@leorw)
25879 * @since 2.1.0
25880 */
25881 function _maybe_add_gdpr_optin_ajax_handler() {
25882 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
25883
25884 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
25885 $this->add_gdpr_optin_ajax_handler_and_style();
25886 }
25887 }
25888
25889 /**
25890 * @author Leo Fajardo (@leorw)
25891 * @since 2.1.0
25892 */
25893 function _fetch_is_marketing_required_flag_value_ajax_action() {
25894 $this->_logger->entrance();
25895
25896 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
25897
25898 $license_key = fs_request_get_raw( 'license_key' );
25899
25900 if ( empty($license_key) ) {
25901 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
25902 }
25903
25904 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
25905 null,
25906 $license_key,
25907 array( $this->_module_id )
25908 );
25909
25910 if ( ! is_array( $user_plugins ) ||
25911 empty($user_plugins) ||
25912 !isset($user_plugins[0]->plugin_id) ||
25913 $user_plugins[0]->plugin_id != $this->_module_id
25914 ) {
25915 /**
25916 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
25917 *
25918 * @author Vova Feldman (@svovaf)
25919 */
25920 self::shoot_ajax_success( array(
25921 'is_marketing_allowed' => null,
25922 'license_owner_id' => null
25923 ) );
25924 }
25925
25926 self::shoot_ajax_success( array(
25927 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
25928 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
25929 ) );
25930 }
25931
25932 /**
25933 * @author Leo Fajardo (@leorw)
25934 * @since 2.3.2
25935 *
25936 * @param number[] $install_ids
25937 *
25938 * @return array {
25939 * An array of objects containing the installs' licenses owners data.
25940 *
25941 * @property number $id User ID.
25942 * @property string $email User email (can be masked email).
25943 * }
25944 */
25945 private function fetch_installs_licenses_owners_data( $install_ids ) {
25946 $this->_logger->entrance();
25947
25948 $response = $this->get_api_user_scope()->get(
25949 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
25950 );
25951
25952 $license_owners = array();
25953
25954 if ( $this->is_api_result_object( $response, 'owners' ) ) {
25955 $license_owners = $response->owners;
25956 }
25957
25958 return $license_owners;
25959 }
25960
25961 /**
25962 * @author Leo Fajardo (@leorw)
25963 * @since 2.1.0
25964 */
25965 private function add_gdpr_optin_ajax_handler_and_style() {
25966 // Add GDPR action AJAX callback.
25967 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
25968
25969 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
25970 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
25971 }
25972
25973 /**
25974 * @author Leo Fajardo (@leorw)
25975 * @since 2.1.0
25976 */
25977 function _gdpr_optin_ajax_action() {
25978 $this->_logger->entrance();
25979
25980 $this->check_ajax_referer( 'gdpr_optin_action' );
25981
25982 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
25983 self::shoot_ajax_failure();
25984 }
25985
25986 $current_wp_user = self::_get_current_wp_user();
25987
25988 $plugin_ids = fs_request_get( 'plugin_ids', array() );
25989 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
25990 self::shoot_ajax_failure();
25991 }
25992
25993 $modules = array_merge(
25994 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
25995 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
25996 );
25997
25998 foreach ( $modules as $key => $module ) {
25999 if ( ! in_array( $module->id, $plugin_ids ) ) {
26000 unset( $modules[ $key ] );
26001 }
26002 }
26003
26004 if ( empty( $modules ) ) {
26005 self::shoot_ajax_failure();
26006 }
26007
26008 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26009
26010 foreach ( $modules as $module ) {
26011 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26012 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26013 ) );
26014 }
26015
26016 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26017
26018 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26019
26020 // 10-year lock.
26021 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26022
26023 self::shoot_ajax_success();
26024 }
26025
26026 /**
26027 * 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.
26028 *
26029 * @author Vova Feldman (@svovaf)
26030 * @since 2.1.0
26031 *
26032 * @return bool
26033 */
26034 private function should_handle_gdpr_admin_notice() {
26035 return $this->apply_filters(
26036 'handle_gdpr_admin_notice',
26037 // Default to false.
26038 false
26039 );
26040 }
26041
26042 #endregion
26043
26044 #----------------------------------------------------------------------------------
26045 #region Marketing
26046 #----------------------------------------------------------------------------------
26047
26048 /**
26049 * Check if current user purchased any other plugins before.
26050 *
26051 * @author Vova Feldman (@svovaf)
26052 * @since 1.0.9
26053 *
26054 * @return bool
26055 */
26056 function has_purchased_before() {
26057 // TODO: Implement has_purchased_before() method.
26058 throw new Exception( 'not implemented' );
26059 }
26060
26061 /**
26062 * Check if current user classified as an agency.
26063 *
26064 * @author Vova Feldman (@svovaf)
26065 * @since 1.0.9
26066 *
26067 * @return bool
26068 */
26069 function is_agency() {
26070 // TODO: Implement is_agency() method.
26071 throw new Exception( 'not implemented' );
26072 }
26073
26074 /**
26075 * Check if current user classified as a developer.
26076 *
26077 * @author Vova Feldman (@svovaf)
26078 * @since 1.0.9
26079 *
26080 * @return bool
26081 */
26082 function is_developer() {
26083 // TODO: Implement is_developer() method.
26084 throw new Exception( 'not implemented' );
26085 }
26086
26087 /**
26088 * Check if current user classified as a business.
26089 *
26090 * @author Vova Feldman (@svovaf)
26091 * @since 1.0.9
26092 *
26093 * @return bool
26094 */
26095 function is_business() {
26096 // TODO: Implement is_business() method.
26097 throw new Exception( 'not implemented' );
26098 }
26099
26100 #endregion
26101
26102 #----------------------------------------------------------------------------------
26103 #region Helper
26104 #----------------------------------------------------------------------------------
26105
26106 /**
26107 * If running with a secret key, assume it's the developer and show pending plans as well.
26108 *
26109 * @author Vova Feldman (@svovaf)
26110 * @since 2.1.2
26111 *
26112 * @param string $path
26113 *
26114 * @return string
26115 */
26116 function add_show_pending( $path ) {
26117 if ( ! $this->has_secret_key() ) {
26118 return $path;
26119 }
26120
26121 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26122 }
26123
26124 #endregion
26125 }
26126