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

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

24,592 lines 924.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 1.1.7.5
114 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
115 */
116 private $_anonymous_mode;
117
118 /**
119 * @since 1.1.9
120 * @var bool Hints the SDK if plugin have any free plans.
121 */
122 private $_is_premium_only;
123
124 /**
125 * @since 1.2.1.6
126 * @var bool Hints the SDK if plugin have premium code version at all.
127 */
128 private $_has_premium_version;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
133 */
134 private $_ignore_pending_mode;
135
136 /**
137 * @since 1.0.8
138 * @var bool Hints the SDK if the plugin has any paid plans.
139 */
140 private $_has_paid_plans;
141
142 /**
143 * @since 1.2.1.5
144 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
145 * without a specified period, if positive - the number of trial days.
146 */
147 private $_trial_days = - 1;
148
149 /**
150 * @since 1.2.1.5
151 * @var bool Hints the SDK if the trial requires a payment method or not.
152 */
153 private $_is_trial_require_payment = false;
154
155 /**
156 * @since 1.0.7
157 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
158 */
159 private $_is_org_compliant;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is has add-ons.
164 */
165 private $_has_addons;
166
167 /**
168 * @since 2.4.5
169 * @var string Navigation type: 'menu' or 'tabs'.
170 */
171 private $_navigation;
172
173 const NAVIGATION_MENU = 'menu';
174 const NAVIGATION_TABS = 'tabs';
175
176 /**
177 * @since 1.1.6
178 * @var string[]bool.
179 */
180 private $_permissions;
181
182 /**
183 * @var FS_Storage
184 */
185 private $_storage;
186
187 /**
188 * @since 1.2.2.7
189 * @var FS_Cache_Manager
190 */
191 private $_cache;
192
193 /**
194 * @since 1.0.0
195 *
196 * @var FS_Logger
197 */
198 private $_logger;
199 /**
200 * @since 1.0.4
201 *
202 * @var FS_Plugin
203 */
204 private $_plugin = false;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin|false
209 */
210 private $_parent_plugin = false;
211 /**
212 * @since 1.1.1
213 *
214 * @var Freemius
215 */
216 private $_parent = false;
217 /**
218 * @since 1.0.1
219 *
220 * @var FS_User
221 */
222 private $_user = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_Site
227 */
228 private $_site = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Plugin_License
233 */
234 private $_license;
235 /**
236 * @since 1.0.2
237 *
238 * @var FS_Plugin_Plan[]
239 */
240 private $_plans = false;
241 /**
242 * @var FS_Plugin_License[]
243 * @since 1.0.5
244 */
245 private $_licenses = false;
246
247 /**
248 * @since 1.0.1
249 *
250 * @var FS_Admin_Menu_Manager
251 */
252 private $_menu;
253
254 /**
255 * @var FS_Admin_Notices
256 */
257 private $_admin_notices;
258
259 /**
260 * @since 1.1.6
261 *
262 * @var FS_Admin_Notices
263 */
264 private static $_global_admin_notices;
265
266 /**
267 * @var FS_Logger
268 * @since 1.0.0
269 */
270 private static $_static_logger;
271
272 /**
273 * @var FS_Options
274 * @since 1.0.2
275 */
276 private static $_accounts;
277
278 /**
279 * @since 1.2.2
280 *
281 * @var number
282 */
283 private $_module_id;
284
285 /**
286 * @var Freemius[]
287 */
288 private static $_instances = array();
289
290 /**
291 * @since 1.2.3
292 *
293 * @var FS_Affiliate
294 */
295 private $affiliate = null;
296
297 /**
298 * @since 1.2.3
299 *
300 * @var FS_AffiliateTerms
301 */
302 private $plugin_affiliate_terms = null;
303
304 /**
305 * @since 1.2.3
306 *
307 * @var FS_AffiliateTerms
308 */
309 private $custom_affiliate_terms = null;
310
311 /**
312 * @since 2.0.0
313 *
314 * @var bool
315 */
316 private $_is_multisite_integrated;
317
318 /**
319 * @since 2.0.0
320 *
321 * @var bool True if the current request is for a network admin screen and the plugin is network active.
322 */
323 private $_is_network_active;
324
325 /**
326 * @since 2.0.0
327 *
328 * @var int|null The original blog ID the plugin was loaded with.
329 */
330 private $_blog_id = null;
331
332 /**
333 * @since 2.0.0
334 *
335 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
336 */
337 private $_context_is_network_or_blog_id = null;
338
339 /**
340 * @since 2.0.0
341 *
342 * @var string
343 */
344 private $_dynamically_added_top_level_page_hook_name = '';
345
346 /**
347 * @author Leo Fajardo (@leorw)
348 * @since 2.3.1
349 *
350 * @var bool
351 */
352 private $is_whitelabeled;
353
354 #region Uninstall Reasons IDs
355
356 const REASON_NO_LONGER_NEEDED = 1;
357 const REASON_FOUND_A_BETTER_PLUGIN = 2;
358 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
359 const REASON_BROKE_MY_SITE = 4;
360 const REASON_SUDDENLY_STOPPED_WORKING = 5;
361 const REASON_CANT_PAY_ANYMORE = 6;
362 const REASON_OTHER = 7;
363 const REASON_DIDNT_WORK = 8;
364 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
365 const REASON_COULDNT_MAKE_IT_WORK = 10;
366 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
367 const REASON_NOT_WORKING = 12;
368 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
369 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
370 const REASON_TEMPORARY_DEACTIVATION = 15;
371
372 #endregion
373
374 /* Ctor
375 ------------------------------------------------------------------------------------------------------------------*/
376
377 /**
378 * Main singleton instance.
379 *
380 * @author Vova Feldman (@svovaf)
381 * @since 1.0.0
382 *
383 * @param number $module_id
384 * @param string|bool $slug
385 * @param bool $is_init Since 1.2.1 Is initiation sequence.
386 */
387 private function __construct( $module_id, $slug = false, $is_init = false ) {
388 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
389 $this->store_id_slug_type_path_map( $module_id, $slug );
390 }
391
392 $this->_module_id = $module_id;
393 $this->_slug = $this->get_slug();
394 $this->_module_type = $this->get_module_type();
395
396 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
397
398 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
399
400 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
401
402 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
403
404 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init );
405 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
406 $this->_plugin_basename = $this->get_plugin_basename();
407 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
408
409 $this->_is_multisite_integrated = (
410 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
411 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
412 );
413
414 $this->_is_network_active = (
415 is_multisite() &&
416 $this->_is_multisite_integrated &&
417 // Themes are always network activated, but the ACTUAL activation is per site.
418 $this->is_plugin() &&
419 (
420 is_plugin_active_for_network( $this->_plugin_basename ) ||
421 // Plugin network level activation or uninstall.
422 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
423 )
424 );
425
426 $this->_storage->set_network_active(
427 $this->_is_network_active,
428 $this->is_delegated_connection()
429 );
430
431 if ( ! isset( $this->_storage->is_network_activated ) ) {
432 $this->_storage->is_network_activated = $this->_is_network_active;
433 }
434
435 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
436 // Update last activation level.
437 $this->_storage->is_network_activated = $this->_is_network_active;
438
439 $this->maybe_adjust_storage();
440 }
441
442 #region Migration
443
444 if ( is_multisite() ) {
445 /**
446 * If the install_timestamp exists on the site level but doesn't exist on the
447 * network level storage, it means that we need to process the storage with migration.
448 *
449 * 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.
450 *
451 * @author Vova Feldman (@svovaf)
452 * @since 2.0.0
453 */
454 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
455 false !== $this->_storage->get( 'install_timestamp', false, false )
456 ) {
457 // Initiate storage migration.
458 $this->_storage->migrate_to_network();
459
460 // Migrate module cache to network level storage.
461 $this->_cache->migrate_to_network();
462 }
463 }
464
465 #endregion
466
467 $base_name_split = explode( '/', $this->_plugin_basename );
468 $this->_plugin_dir_name = $base_name_split[0];
469
470 if ( $this->_logger->is_on() ) {
471 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
472 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
473 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
474 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
475 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
476 }
477
478 // Remember link between file to slug.
479 $this->store_file_slug_map();
480
481 // Store plugin's initial install timestamp.
482 if ( ! isset( $this->_storage->install_timestamp ) ) {
483 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
484 }
485
486 if ( ! is_object( $this->_plugin ) ) {
487 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
488 }
489
490 $this->_admin_notices = FS_Admin_Notices::instance(
491 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
492 /**
493 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
494 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
495 *
496 * @author Leo Fajardo (@leorw)
497 * @since 1.2.2
498 */
499 ( is_object( $this->_plugin ) ? $this->_plugin->title : $this->get_plugin_name() ),
500 $this->get_unique_affix()
501 );
502
503 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
504 fs_request_is_action( 'restart_freemius' )
505 ) {
506 FS_Api::clear_cache();
507 $this->_cache->clear();
508 }
509
510 $this->register_constructor_hooks();
511
512 /**
513 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
514 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
515 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
516 *
517 * @author Leo Fajardo (@leorw)
518 */
519 self::migrate_install_plan_to_plan_id( $this->_storage );
520
521 $this->_load_account();
522
523 $this->_version_updates_handler();
524 }
525
526 /**
527 * @author Leo Fajardo (@leorw)
528 * @since 2.3.0
529 */
530 private function maybe_adjust_storage() {
531 $install_timestamp = null;
532 $prev_is_premium = null;
533
534 $options_to_update = array();
535
536 $is_network_admin = fs_is_network_admin();
537
538 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
539
540 if ( ! $is_network_admin ) {
541 if ( is_null( $network_install_timestamp ) ) {
542 // Plugin was not network-activated before.
543 return;
544 }
545
546 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
547 // Set the `install_timestamp` only if it's not yet set.
548 $install_timestamp = $network_install_timestamp;
549 }
550
551 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
552 } else {
553 $current_wp_user = self::_get_current_wp_user();
554 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
555 $network_user_info = array();
556
557 $skips_count = 0;
558
559 $sites = self::get_sites();
560 $sites_count = count( $sites );
561
562 $blog_id_2_install_map = array();
563
564 $is_first_non_ignored_blog = true;
565
566 foreach ( $sites as $site ) {
567 $blog_id = self::get_site_blog_id( $site );
568
569 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
570
571 if ( is_null( $blog_install_timestamp ) ) {
572 // Plugin has not been installed on this blog.
573 continue;
574 }
575
576 $is_earlier_install = (
577 ! is_null( $install_timestamp ) &&
578 $blog_install_timestamp < $install_timestamp
579 );
580
581 $install = $this->get_install_by_blog_id( $blog_id );
582
583 $update_network_user_info = false;
584
585 if ( ! is_object( $install ) ) {
586 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
587 // The opt-in decision (whether to skip or opt in) is yet to be made.
588 continue;
589 }
590
591 $skips_count ++;
592 } else {
593 $blog_id_2_install_map[ $blog_id ] = $install;
594
595 if ( empty( $network_user_info ) ) {
596 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
597 $update_network_user_info = true;
598 }
599
600 if ( ! $update_network_user_info &&
601 is_object( $current_fs_user ) &&
602 $network_user_info['user_id'] != $current_fs_user->id &&
603 $install->user_id == $current_fs_user->id
604 ) {
605 // If an install that is owned by the current WP user is found, use its user information instead.
606 $update_network_user_info = true;
607 }
608
609 if ( ! $update_network_user_info &&
610 $is_earlier_install &&
611 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
612 ) {
613 // 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.
614 $update_network_user_info = true;
615 }
616 }
617
618 if ( $update_network_user_info ) {
619 $network_user_info = array(
620 'user_id' => $install->user_id,
621 'blog_id' => $blog_id
622 );
623 }
624
625 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
626
627 if ( $is_first_non_ignored_blog ) {
628 $prev_is_premium = $site_prev_is_premium;
629
630 if ( is_null( $network_install_timestamp ) ) {
631 $install_timestamp = $blog_install_timestamp;
632 }
633
634 $is_first_non_ignored_blog = false;
635
636 continue;
637 }
638
639 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
640 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
641 $prev_is_premium = null;
642 }
643
644 if ( $is_earlier_install ) {
645 // If an earlier install timestamp is found.
646 $install_timestamp = $blog_install_timestamp;
647 }
648 }
649
650 $installs_count = count( $blog_id_2_install_map );
651
652 if ( $sites_count === ( $installs_count + $skips_count ) ) {
653 if ( ! empty( $network_user_info ) ) {
654 $options_to_update['network_user_id'] = $network_user_info['user_id'];
655 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
656
657 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
658 if ( $install->user_id == $network_user_info['user_id'] ) {
659 continue;
660 }
661
662 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
663 }
664 }
665
666 if ( $sites_count === $skips_count ) {
667 /**
668 * Assume network-level skipping as the intended action if all actions identified were only
669 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
670 */
671 $options_to_update['is_anonymous_ms'] = true;
672 } else if ( $sites_count === $installs_count ) {
673 /**
674 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
675 * (i.e., no delegation and skipping of the connections so far).
676 */
677 $options_to_update['is_network_connected'] = true;
678 }
679 }
680 }
681
682 if ( ! is_null( $install_timestamp ) ) {
683 $options_to_update['install_timestamp'] = $install_timestamp;
684 }
685
686 if ( ! is_null( $prev_is_premium ) ) {
687 $options_to_update['prev_is_premium'] = $prev_is_premium;
688 }
689
690 if ( ! empty( $options_to_update ) ) {
691 $this->adjust_storage( $options_to_update, $is_network_admin );
692 }
693 }
694
695 /**
696 * @author Leo Fajardo (@leorw)
697 * @since 2.3.0
698 *
699 * @param array $options
700 * @param bool $is_network_admin
701 */
702 private function adjust_storage( $options, $is_network_admin ) {
703 foreach ( $options as $name => $value ) {
704 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
705 }
706 }
707
708 /**
709 * Checks whether this module has a settings menu.
710 *
711 * @author Leo Fajardo (@leorw)
712 * @since 1.2.2
713 *
714 * @return bool
715 */
716 function has_settings_menu() {
717 return ( $this->_is_network_active && fs_is_network_admin() ) ?
718 $this->_menu->has_network_menu() :
719 $this->_menu->has_menu();
720 }
721
722 /**
723 * 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.
724 *
725 * @author Vova Feldman (@svovaf)
726 * @since 2.4.5
727 *
728 * @return bool
729 */
730 function show_opt_in_on_themes_page() {
731 if ( ! $this->is_free_wp_org_theme() ) {
732 return false;
733 }
734
735 if ( ! $this->has_settings_menu() ) {
736 return true;
737 }
738
739 return $this->show_settings_with_tabs();
740 }
741
742 /**
743 * If `true` the opt-in should be shown on the product's main setting page.
744 *
745 * @author Vova Feldman (@svovaf)
746 * @since 2.4.5
747 *
748 * @return bool
749 *
750 * @uses show_opt_in_on_themes_page();
751 */
752 function show_opt_in_on_setting_page() {
753 return ! $this->show_opt_in_on_themes_page();
754 }
755
756 /**
757 * If `true` the settings should be shown using tabs.
758 *
759 * @author Vova Feldman (@svovaf)
760 * @since 2.4.5
761 *
762 * @return bool
763 */
764 function show_settings_with_tabs() {
765 return ( self::NAVIGATION_TABS === $this->_navigation );
766 }
767
768 /**
769 * Check if the context module is free wp.org theme.
770 *
771 * This method is helpful because:
772 * 1. wp.org themes are limited to a single submenu item,
773 * and sub-submenu items are most likely not allowed (never verified).
774 * 2. wp.org themes are not allowed to redirect the user
775 * after the theme activation, therefore, the agreed UX
776 * is showing the opt-in as a modal dialog box after
777 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
778 *
779 * @author Vova Feldman (@svovaf)
780 * @since 1.2.2.7
781 *
782 * @return bool
783 */
784 function is_free_wp_org_theme() {
785 return (
786 $this->is_theme() &&
787 $this->is_org_repo_compliant() &&
788 ! $this->is_premium()
789 );
790 }
791
792 /**
793 * Checks whether this a submenu item is visible.
794 *
795 * @author Vova Feldman (@svovaf)
796 * @since 1.2.2.6
797 * @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.
798 *
799 * @param string $slug
800 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
801 *
802 * @return bool
803 */
804 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
805 if ( $this->is_admin_page( $slug ) ) {
806 /**
807 * It is the current context page, so show the submenu item
808 * so the user will have the right context page, even if it
809 * was set to hidden.
810 */
811 return true;
812 }
813
814 if ( ! $this->has_settings_menu() ) {
815 // No menu settings at all.
816 return false;
817 }
818
819 if ( ! $is_tabs_visibility_check && $this->show_settings_with_tabs() ) {
820 /**
821 * wp.org themes are limited to a single submenu item, and
822 * sub-submenu items are most likely not allowed (never verified).
823 */
824 return false;
825 }
826
827 return $this->_menu->is_submenu_item_visible( $slug );
828 }
829
830 /**
831 * Check if a Freemius page should be accessible via the UI.
832 *
833 * @author Vova Feldman (@svovaf)
834 * @since 1.2.2.7
835 *
836 * @param string $slug
837 *
838 * @return bool
839 */
840 function is_page_visible( $slug ) {
841 if ( $this->is_admin_page( $slug ) ) {
842 return true;
843 }
844
845 return $this->_menu->is_submenu_item_visible( $slug, true, true );
846 }
847
848 /**
849 * @author Vova Feldman (@svovaf)
850 * @since 1.0.9
851 */
852 private function _version_updates_handler() {
853 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
854 // Freemius version upgrade mode.
855 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
856 $this->_storage->sdk_version = $this->version;
857
858 if ( empty( $this->_storage->sdk_last_version ) ||
859 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
860 ) {
861 $this->_storage->sdk_upgrade_mode = true;
862 $this->_storage->sdk_downgrade_mode = false;
863 } else {
864 $this->_storage->sdk_downgrade_mode = true;
865 $this->_storage->sdk_upgrade_mode = false;
866
867 }
868
869 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
870 }
871
872 $plugin_version = $this->get_plugin_version();
873 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
874 // Plugin version upgrade mode.
875 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
876 $this->_storage->plugin_version = $plugin_version;
877
878 if ( empty( $this->_storage->plugin_last_version ) ||
879 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
880 ) {
881 $this->_storage->plugin_upgrade_mode = true;
882 $this->_storage->plugin_downgrade_mode = false;
883 } else {
884 $this->_storage->plugin_downgrade_mode = true;
885 $this->_storage->plugin_upgrade_mode = false;
886 }
887
888 if ( ! empty( $this->_storage->plugin_last_version ) ) {
889 // Different version of the plugin was installed before, therefore it's an update.
890 $this->_storage->is_plugin_new_install = false;
891 }
892
893 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
894 }
895 }
896
897 #--------------------------------------------------------------------------------
898 #region Data Migration on SDK Update
899 #--------------------------------------------------------------------------------
900
901 /**
902 * @author Vova Feldman (@svovaf)
903 * @since 1.1.5
904 *
905 * @param string $sdk_prev_version
906 * @param string $sdk_version
907 */
908 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
909 /**
910 * @since 1.1.7.3 Fixed unwanted connectivity test cleanup.
911 */
912 if ( empty( $sdk_prev_version ) ) {
913 return;
914 }
915
916 if ( version_compare( $sdk_prev_version, '2.1.0', '<' ) &&
917 version_compare( $sdk_version, '2.1.0', '>=' )
918 ) {
919 $this->_storage->handle_gdpr_admin_notice = true;
920 }
921
922 if ( version_compare( $sdk_prev_version, '2.0.0', '<' ) &&
923 version_compare( $sdk_version, '2.0.0', '>=' )
924 ) {
925 $this->migrate_to_subscriptions_collection();
926
927 $this->consolidate_licenses();
928
929 // Clear trial_plan since it's now loaded from the plans collection when needed.
930 $this->_storage->remove( 'trial_plan', true, false );
931 }
932
933 if ( version_compare( $sdk_prev_version, '1.2.3', '<' ) &&
934 version_compare( $sdk_version, '1.2.3', '>=' )
935 ) {
936 /**
937 * Starting from version 1.2.3, paths are stored as relative instead of absolute and some of them can be
938 * invalid.
939 *
940 * @author Leo Fajardo (@leorw)
941 */
942 $this->remove_invalid_paths();
943 }
944
945 if ( version_compare( $sdk_prev_version, '1.1.5', '<' ) &&
946 version_compare( $sdk_version, '1.1.5', '>=' )
947 ) {
948 // On version 1.1.5 merged connectivity and is_on data.
949 if ( isset( $this->_storage->connectivity_test ) ) {
950 if ( ! isset( $this->_storage->is_on ) ) {
951 unset( $this->_storage->connectivity_test );
952 } else {
953 $connectivity_data = $this->_storage->connectivity_test;
954 $connectivity_data['is_active'] = $this->_storage->is_on['is_active'];
955 $connectivity_data['timestamp'] = $this->_storage->is_on['timestamp'];
956
957 // Override.
958 $this->_storage->connectivity_test = $connectivity_data;
959
960 // Remove previous structure.
961 unset( $this->_storage->is_on );
962 }
963
964 }
965 }
966
967 if (
968 version_compare( $sdk_prev_version, '2.2.1', '<' ) &&
969 version_compare( $sdk_version, '2.2.1', '>=' )
970 ) {
971 /**
972 * Clear the file cache without storing the previous path since it could be a wrong path. For example,
973 * in the versions of the SDK lower than 2.2.1, it's possible for the path of an add-on to be the same
974 * as the parent plugin's when the add-on was auto-installed since the relevant method names were not
975 * skipped in the logic that determines the right path in the `get_caller_main_file_and_type` method
976 * (e.g. `try_activate_plugin`). Since it was an auto-installation, the caller was the parent plugin
977 * and so its path was used. In case the stored path is wrong, clearing the cache will resolve issues
978 * related to data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
979 *
980 * @author Leo Fajardo (@leorw)
981 * @since 2.2.1
982 */
983 $this->clear_module_main_file_cache( false );
984 }
985 }
986
987 /**
988 * @author Leo Fajardo (@leorw)
989 * @since 2.0.0
990 *
991 * @param \FS_Storage $storage
992 * @param bool|int|null $blog_id
993 */
994 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
995 if ( empty( $storage->sdk_version ) ) {
996 // New installation of the plugin, no need to upgrade.
997 return;
998 }
999
1000 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
1001 // Previous version is >= 2.0.0, so no need to migrate.
1002 return;
1003 }
1004
1005 // Alias.
1006 $module_type = $storage->get_module_type();
1007 $module_slug = $storage->get_module_slug();
1008
1009 $installs = self::get_all_sites( $module_type, $blog_id );
1010 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
1011
1012 if ( ! is_object( $install ) ) {
1013 return;
1014 }
1015
1016 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
1017 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
1018 $install->plan_id = self::_decrypt( $install->plan->id );
1019 }
1020
1021 unset( $install->plan );
1022
1023 $installs[ $module_slug ] = clone $install;
1024
1025 self::set_account_option_by_module(
1026 $module_type,
1027 'sites',
1028 $installs,
1029 true,
1030 $blog_id
1031 );
1032 }
1033 }
1034
1035 /**
1036 * @author Leo Fajardo (@leorw)
1037 * @since 2.0.0
1038 */
1039 private function migrate_to_subscriptions_collection() {
1040 if ( ! is_object( $this->_site ) ) {
1041 return;
1042 }
1043
1044 if ( isset( $this->_storage->subscription ) && is_object( $this->_storage->subscription ) ) {
1045 $this->_storage->subscriptions = array( fs_get_entity( $this->_storage->subscription, FS_Subscription::get_class_name() ) );
1046 }
1047 }
1048
1049 /**
1050 * @author Leo Fajardo (@leorw)
1051 * @since 2.0.0
1052 */
1053 private function consolidate_licenses() {
1054 $plugin_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_PLUGIN );
1055 if ( isset( $plugin_licenses[ $this->_slug ] ) ) {
1056 $plugin_licenses = $plugin_licenses[ $this->_slug ];
1057 } else {
1058 $plugin_licenses = array();
1059 }
1060
1061 $theme_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_THEME );
1062 if ( isset( $theme_licenses[ $this->_slug ] ) ) {
1063 $theme_licenses = $theme_licenses[ $this->_slug ];
1064 } else {
1065 $theme_licenses = array();
1066 }
1067
1068 if ( empty( $plugin_licenses ) && empty( $theme_licenses ) ) {
1069 return;
1070 }
1071
1072 $all_licenses = array();
1073 $user_id_license_ids_map = array();
1074
1075 foreach ( $plugin_licenses as $user_id => $user_licenses ) {
1076 if ( is_array( $user_licenses ) ) {
1077 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
1078 $user_id_license_ids_map[ $user_id ] = array();
1079 }
1080
1081 foreach ( $user_licenses as $user_license ) {
1082 $all_licenses[] = $user_license;
1083 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
1084 }
1085 }
1086 }
1087
1088 foreach ( $theme_licenses as $user_id => $user_licenses ) {
1089 if ( is_array( $user_licenses ) ) {
1090 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
1091 $user_id_license_ids_map[ $user_id ] = array();
1092 }
1093
1094 foreach ( $user_licenses as $user_license ) {
1095 $all_licenses[] = $user_license;
1096 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
1097 }
1098 }
1099 }
1100
1101 self::store_user_id_license_ids_map(
1102 $user_id_license_ids_map,
1103 $this->_module_id
1104 );
1105
1106 $this->_store_licenses( true, $this->_module_id, $all_licenses );
1107 }
1108
1109 /**
1110 * Remove invalid paths.
1111 *
1112 * @author Leo Fajardo (@leorw)
1113 * @since 1.2.3
1114 */
1115 private function remove_invalid_paths() {
1116 // Remove invalid path that is still associated with the current slug if there's any.
1117 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
1118 foreach ( $file_slug_map as $plugin_basename => $slug ) {
1119 if ( $slug === $this->_slug &&
1120 $plugin_basename !== $this->_plugin_basename &&
1121 ! file_exists( $this->get_absolute_path( $plugin_basename ) )
1122 ) {
1123 unset( $file_slug_map[ $plugin_basename ] );
1124 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
1125
1126 break;
1127 }
1128 }
1129 }
1130
1131 /**
1132 * @author Vova Feldman (@svovaf)
1133 * @since 1.2.2.7
1134 *
1135 * @param string $plugin_prev_version
1136 * @param string $plugin_version
1137 */
1138 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1139 if ( $this->is_theme() ) {
1140 // Expire the cache of the previous tabs since the theme may
1141 // have setting updates.
1142 $this->_cache->expire( 'tabs' );
1143 $this->_cache->expire( 'tabs_stylesheets' );
1144 }
1145 }
1146
1147 /**
1148 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1149 * - Moves some data to the network level storage.
1150 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1151 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1152 * - 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.
1153 * - 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.
1154 * - 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.
1155 *
1156 * @author Vova Feldman (@svovaf)
1157 * @since 2.0.0
1158 */
1159 private static function migrate_accounts_to_network() {
1160 $sites = self::get_sites();
1161 $sites_count = count( $sites );
1162 $connection_status = array();
1163 $plugin_slugs = array();
1164 foreach ( $sites as $site ) {
1165 $blog_id = self::get_site_blog_id( $site );
1166
1167 self::$_accounts->migrate_to_network( $blog_id );
1168
1169 /**
1170 * Build a list of all Freemius powered plugins slugs.
1171 */
1172 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1173 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1174 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1175 $plugin_slugs[ $data['slug'] ] = true;
1176 }
1177 }
1178
1179 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1180
1181 if ( is_array( $installs ) ) {
1182 foreach ( $installs as $slug => $install ) {
1183 if ( ! isset( $connection_status[ $slug ] ) ) {
1184 $connection_status[ $slug ] = array();
1185 }
1186
1187 if ( is_object( $install ) &&
1188 FS_Site::is_valid_id( $install->id ) &&
1189 FS_User::is_valid_id( $install->user_id )
1190 ) {
1191 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1192 }
1193 }
1194 }
1195 }
1196
1197 foreach ( $plugin_slugs as $slug => $true ) {
1198 if ( ! isset( $connection_status[ $slug ] ) ) {
1199 $connection_status[ $slug ] = array();
1200 }
1201
1202 foreach ( $sites as $site ) {
1203 $blog_id = self::get_site_blog_id( $site );
1204
1205 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1206 continue;
1207 }
1208
1209 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1210
1211 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1212
1213 if ( ! is_null( $is_anonymous ) ) {
1214 // Since 1.1.3 is_anonymous is an array.
1215 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1216 $is_anonymous = $is_anonymous['is'];
1217 }
1218
1219 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1220 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1221 }
1222 }
1223
1224 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1225 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1226 }
1227 }
1228 }
1229
1230 $super_admins = array();
1231
1232 foreach ( $connection_status as $slug => $blogs_status ) {
1233 $skips = 0;
1234 $ignores = 0;
1235 $connections = 0;
1236 $opted_in_users = array();
1237 $opted_in_super_admins = array();
1238
1239 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1240
1241 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1242 if ( 'skipped' === $status_or_user_id ) {
1243 $skips ++;
1244 } else if ( 'ignored' === $status_or_user_id ) {
1245 $ignores ++;
1246 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1247 $connections ++;
1248
1249 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1250 $opted_in_users[ $status_or_user_id ] = array();
1251 }
1252
1253 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1254
1255 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1256 self::is_super_admin( $status_or_user_id )
1257 ) {
1258 // Cache super-admin data.
1259 $super_admins[ $status_or_user_id ] = true;
1260
1261 // Remember opted-in super-admins for the plugin.
1262 $opted_in_super_admins[ $status_or_user_id ] = true;
1263 }
1264 }
1265 }
1266
1267 $main_super_admin_user_id = null;
1268 $all_migrated = false;
1269 if ( $sites_count == $skips ) {
1270 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1271 $storage->is_anonymous_ms = $storage->is_anonymous;
1272
1273 $all_migrated = true;
1274 } else if ( $sites_count == $ignores ) {
1275 // Don't do anything, still in activation mode.
1276
1277 $all_migrated = true;
1278 } else if ( 0 < count( $opted_in_super_admins ) ) {
1279 // Find the super-admin with the majority of installs.
1280 $max_installs_by_super_admin = 0;
1281 foreach ( $opted_in_super_admins as $user_id => $true ) {
1282 $installs_count = count( $opted_in_users[ $user_id ] );
1283
1284 if ( $installs_count > $max_installs_by_super_admin ) {
1285 $max_installs_by_super_admin = $installs_count;
1286 $main_super_admin_user_id = $user_id;
1287 }
1288 }
1289
1290 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1291 // Super-admin opted-in for all sites in the network.
1292 $storage->is_network_connected = true;
1293
1294 $all_migrated = true;
1295 }
1296
1297 // Store network user.
1298 $storage->network_user_id = $main_super_admin_user_id;
1299
1300 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1301 // Since all sites are opted-in, associating with the main site.
1302 get_current_blog_id() :
1303 // Associating with the 1st found opted-in site.
1304 $opted_in_users[ $main_super_admin_user_id ][0];
1305
1306 /**
1307 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1308 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1309 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1310 * will be empty.
1311 */
1312 $storage->migrate_to_network();
1313 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1314 } else {
1315 // At least one opt-in. All the opt-in were created by a non-super-admin.
1316 if ( 0 == $ignores ) {
1317 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1318 $storage->store( 'is_delegated_connection', true, true );
1319
1320 $all_migrated = true;
1321 }
1322 }
1323
1324 if ( ! $all_migrated ) {
1325 /**
1326 * Delegate all sites that were:
1327 * 1) Opted-in by a user that is NOT the main-super-admin.
1328 * 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.
1329 */
1330 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1331 if ( $status_or_user_id == $main_super_admin_user_id ) {
1332 continue;
1333 }
1334
1335 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1336 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1337 ) {
1338 $storage->store( 'is_delegated_connection', true, $blog_id );
1339 }
1340 }
1341 }
1342
1343
1344 if ( ( $connections + $skips > 0 ) ) {
1345 if ( $ignores > 0 ) {
1346 /**
1347 * If admin already opted-in or skipped in any of the network sites, and also
1348 * have sites which the connection decision was not yet taken, set this plugin
1349 * into network activation mode so the super-admin can choose what to do with
1350 * the rest of the sites.
1351 */
1352 self::set_network_upgrade_mode( $storage );
1353 }
1354 }
1355 }
1356 }
1357
1358 /**
1359 * Set a module into network upgrade mode.
1360 *
1361 * @author Vova Feldman (@svovaf)
1362 * @since 2.0.0
1363 *
1364 * @param \FS_Storage $storage
1365 *
1366 * @return bool
1367 */
1368 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1369 return $storage->is_network_activation = true;
1370 }
1371
1372 /**
1373 * Will return true after upgrading to the SDK with the network level integration,
1374 * when the super-admin involvement is required regarding the rest of the sites.
1375 *
1376 * @author Vova Feldman (@svovaf)
1377 * @since 2.0.0
1378 *
1379 * @return bool
1380 */
1381 function is_network_upgrade_mode() {
1382 return $this->_storage->get( 'is_network_activation' );
1383 }
1384
1385 /**
1386 * Clear flag after the upgrade mode completion.
1387 *
1388 * @author Vova Feldman (@svovaf)
1389 * @since 2.0.0
1390 *
1391 * @return bool True if network activation was on and now completed.
1392 */
1393 private function network_upgrade_mode_completed() {
1394 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1395 $this->_storage->remove( 'is_network_activation' );
1396
1397 return true;
1398 }
1399
1400 return false;
1401 }
1402
1403 #endregion
1404
1405 /**
1406 * This action is connected to the 'plugins_loaded' hook and helps to determine
1407 * if this is a new plugin installation or a plugin update.
1408 *
1409 * There are 3 different use-cases:
1410 * 1) New plugin installation right with Freemius:
1411 * 1.1 _activate_plugin_event_hook() will be executed first
1412 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1413 * and $this->_storage->plugin_last_version is not set,
1414 * $this->_storage->is_plugin_new_install will be set to TRUE.
1415 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1416 * be already set to TRUE.
1417 *
1418 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1419 * 2.1 _activate_plugin_event_hook() will not be executed, because
1420 * the activation hook do NOT fires on updates since WP 3.1.
1421 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1422 * be empty, therefore, it will be set to FALSE.
1423 *
1424 * 3) Plugin update, had Freemius in prev version as well:
1425 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1426 * before, $this->_storage->plugin_last_version will NOT be empty,
1427 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1428 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1429 * already set, therefore, it will not be modified.
1430 *
1431 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1432 *
1433 * NOTE:
1434 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1435 * and then, the next immediate PageView is the plugin's main settings page, it will not
1436 * show the opt-in right away. The reason it will happen is because Freemius execution
1437 * will be turned off till the plugin is fully loaded at least once
1438 * (till $this->_storage->was_plugin_loaded is TRUE).
1439 *
1440 * @author Vova Feldman (@svovaf)
1441 * @since 1.1.9
1442 *
1443 */
1444 function _plugins_loaded() {
1445 // Update flag that plugin was loaded with Freemius at least once.
1446 $this->_storage->was_plugin_loaded = true;
1447
1448 /**
1449 * Bug fix - only set to false when it's a plugin, due to the
1450 * execution sequence of the theme hooks and our methods, if
1451 * this will be set for themes, Freemius will always assume
1452 * it's a theme update.
1453 *
1454 * @author Vova Feldman (@svovaf)
1455 * @since 1.2.2.2
1456 */
1457 if ( $this->is_plugin() &&
1458 ! isset( $this->_storage->is_plugin_new_install )
1459 ) {
1460 $this->_storage->is_plugin_new_install = false;
1461 }
1462 }
1463
1464 /**
1465 * Add special parameter to WP admin AJAX calls so when we
1466 * process AJAX calls we can identify its source properly.
1467 *
1468 * @author Leo Fajardo (@leorw)
1469 * @since 2.0.0
1470 */
1471 static function _enrich_ajax_url() {
1472 $admin_param = is_network_admin() ?
1473 '_fs_network_admin' :
1474 '_fs_blog_admin';
1475 ?>
1476 <script type="text/javascript">
1477 (function ($) {
1478 $(document).ajaxSend(function (event, jqxhr, settings) {
1479 if (settings.url &&
1480 -1 < settings.url.indexOf('admin-ajax.php') &&
1481 ! ( settings.url.indexOf( '<?php echo $admin_param ?>' ) > 0 )
1482 ) {
1483 if (settings.url.indexOf('?') > 0) {
1484 settings.url += '&';
1485 } else {
1486 settings.url += '?';
1487 }
1488
1489 settings.url += '<?php echo $admin_param ?>=true';
1490
1491 }
1492 });
1493 })(jQuery);
1494 </script>
1495 <?php
1496 }
1497
1498 /**
1499 * Opens the support forum subemenu item in a new browser page.
1500 *
1501 * @author Vova Feldman (@svovaf)
1502 * @since 2.1.4
1503 */
1504 static function _open_support_forum_in_new_page() {
1505 ?>
1506 <script type="text/javascript">
1507 (function ($) {
1508 $('.fs-submenu-item.wp-support-forum').parent().attr('target', '_blank');
1509 })(jQuery);
1510 </script>
1511 <?php
1512 }
1513
1514 /**
1515 * @author Vova Feldman (@svovaf)
1516 * @since 1.0.9
1517 */
1518 private function register_constructor_hooks() {
1519 $this->_logger->entrance();
1520
1521 if ( is_admin() ) {
1522 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1523
1524 if ( $this->is_plugin() ) {
1525 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1526 /**
1527 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1528 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1529 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1530 * updating of a .org plugin).
1531 */
1532 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1533 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1534 /**
1535 * 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.
1536 *
1537 * @author Leo Fajardo (@leorw)
1538 * @since 2.2.3
1539 */
1540 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1541 }
1542
1543 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1544
1545 /**
1546 * @since 1.2.2
1547 *
1548 * Hook to both free and premium version activations to support
1549 * auto deactivation on the other version activation.
1550 */
1551 register_activation_hook(
1552 $plugin_dir . $this->_free_plugin_basename,
1553 array( &$this, '_activate_plugin_event_hook' )
1554 );
1555
1556 register_activation_hook(
1557 $plugin_dir . $this->premium_plugin_basename(),
1558 array( &$this, '_activate_plugin_event_hook' )
1559 );
1560 } else {
1561 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1562
1563 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1564 }
1565
1566 /**
1567 * Part of the mechanism to identify new plugin install vs. plugin update.
1568 *
1569 * @author Vova Feldman (@svovaf)
1570 * @since 1.1.9
1571 */
1572 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1573 /**
1574 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1575 * when the logic gets here since the activation logic first add the activate plugins,
1576 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1577 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1578 * plugin activation, and that IS intentional.
1579 *
1580 * @author Vova Feldman (@svovaf)
1581 */
1582 if ( $this->is_plugin() &&
1583 $this->is_activation_mode( false ) &&
1584 0 == did_action( 'plugins_loaded' )
1585 ) {
1586 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1587 } else {
1588 // If was activated before, then it was already loaded before.
1589 $this->_plugins_loaded();
1590 }
1591 }
1592
1593 if ( ! self::is_ajax() ) {
1594 if ( ! $this->is_addon() ) {
1595 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1596 }
1597 }
1598
1599 if ( $this->_storage->handle_gdpr_admin_notice ) {
1600 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1601 }
1602
1603 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1604 }
1605
1606 if ( $this->is_plugin() ) {
1607 if ( $this->_is_network_active ) {
1608 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1609 }
1610
1611 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1612 }
1613
1614 if ( is_multisite() ) {
1615 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1616 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1617 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1618 add_action( 'deleted_blog', array( &$this, '_after_site_deleted_callback' ), 10, 2 );
1619
1620 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1621 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1622 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1623 }
1624
1625 if ( $this->is_theme() &&
1626 self::is_customizer() &&
1627 $this->apply_filters( 'show_customizer_upsell', true )
1628 ) {
1629 // Register customizer upsell.
1630 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1631 }
1632
1633 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1634
1635 if ( $this->is_theme() ) {
1636 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1637 }
1638
1639 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1640 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1641 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1642 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1643
1644 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1645 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1646 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1647
1648 if ( $this->_is_network_active && fs_is_network_admin() ) {
1649 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1650 }
1651
1652 $this->add_ajax_action( 'install_premium_version', array(
1653 &$this,
1654 '_install_premium_version_ajax_action'
1655 ) );
1656
1657 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1658
1659 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1660
1661 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1662
1663 $this->add_action(
1664 'plugin_version_update',
1665 array( &$this, '_after_version_update' ),
1666 WP_FS__DEFAULT_PRIORITY,
1667 2
1668 );
1669 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1670
1671 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
1672 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
1673 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1674
1675 /**
1676 * Handle request to reset anonymous mode for `get_reconnect_url()`.
1677 *
1678 * @author Vova Feldman (@svovaf)
1679 * @since 1.2.1.5
1680 */
1681 if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
1682 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
1683 ) {
1684 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1685 }
1686 }
1687
1688 /**
1689 * Register the required hooks right after the settings parse is completed.
1690 *
1691 * @author Vova Feldman (@svovaf)
1692 * @since 2.3.1
1693 */
1694 private function register_after_settings_parse_hooks() {
1695 if ( is_admin() &&
1696 $this->is_theme() &&
1697 $this->is_premium() &&
1698 ! $this->has_active_valid_license()
1699 ) {
1700 $this->add_ajax_action(
1701 'delete_theme_update_data',
1702 array( &$this, '_delete_theme_update_data_action' )
1703 );
1704 }
1705
1706 if ( $this->show_settings_with_tabs() ) {
1707 /**
1708 * Include the required hooks to capture the theme settings' page tabs
1709 * and cache them.
1710 *
1711 * @author Vova Feldman (@svovaf)
1712 * @since 1.2.2.7
1713 */
1714 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1715 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1716 // Add license activation AJAX callback.
1717 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1718
1719 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1720 }
1721
1722 add_action(
1723 'admin_footer',
1724 array( &$this, '_add_freemius_tabs' ),
1725 /**
1726 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1727 * That's why the priority is 11 while the tabs capture logic is added
1728 * with priority 10.
1729 *
1730 * @author Vova Feldman (@svovaf)
1731 */
1732 11
1733 );
1734 }
1735
1736 if ( ! self::is_ajax() ) {
1737 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1738 add_action(
1739 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1740 array( &$this, '_prepare_admin_menu' ),
1741 WP_FS__LOWEST_PRIORITY
1742 );
1743 }
1744 }
1745 }
1746
1747 /**
1748 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1749 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1750 *
1751 * @author Leo Fajardo (@leorw)
1752 * @since 2.2.3
1753 *
1754 * @param object $updates
1755 * @param string|null $transient
1756 *
1757 * @return object
1758 */
1759 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1760 if ( is_object( $updates ) && isset( $updates->response ) ) {
1761 foreach ( $updates->response as $file => $plugin ) {
1762 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1763 unset( $updates->response[ $file ] );
1764 }
1765 }
1766 }
1767
1768 return $updates;
1769 }
1770
1771 /**
1772 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1773 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1774 *
1775 * @author Leo Fajardo (@leorw)
1776 * @since 2.2.3
1777 *
1778 * @return string
1779 */
1780 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1781 $slug_basename_map = array();
1782 foreach ( self::$_instances as $instance ) {
1783 if ( ! $instance->is_plugin() ) {
1784 continue;
1785 }
1786
1787 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1788 }
1789 ?>
1790 <script type="text/javascript">
1791 (function( $ ) {
1792 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1793 for ( var slug in slugBasenameMap ) {
1794 var basename = slugBasenameMap[ slug ];
1795
1796 // Try to get the plugin rows if on the "Plugins" page.
1797 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1798
1799 if ( 0 === $pluginRows.length ) {
1800 // Try to get the plugin rows if on the "Updates" page.
1801 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1802 if ( 0 !== $pluginCheckbox.length ) {
1803 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1804 }
1805 }
1806
1807 if ( 0 === $pluginRows.length ) {
1808 // No plugin rows found.
1809 continue;
1810 }
1811
1812 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1813 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1814 var $this = $( this ),
1815 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1816
1817 $this.attr( 'href', href );
1818 });
1819 }
1820 })( jQuery );
1821 </script>
1822 <?php
1823 }
1824
1825 /**
1826 * @author Leo Fajardo (@leorw)
1827 * @since 2.3.0
1828 */
1829 static function _maybe_add_beta_label_styles() {
1830 $has_any_beta_version = false;
1831
1832 foreach ( self::$_instances as $instance ) {
1833 if ( $instance->is_beta() ) {
1834 $has_any_beta_version = true;
1835 break;
1836 }
1837 }
1838
1839 if ( $has_any_beta_version ) {
1840 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1841 }
1842 }
1843
1844 /**
1845 * @author Leo Fajardo (@leorw)
1846 * @since 2.3.0
1847 */
1848 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1849 $beta_data = array();
1850
1851 foreach ( self::$_instances as $instance ) {
1852 if ( ! $instance->is_premium() ) {
1853 continue;
1854 }
1855
1856 /**
1857 * If there's an available beta version update, a confirmation message will be shown when the
1858 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1859 */
1860 $has_beta_update = $instance->has_beta_update();
1861
1862 $is_beta = (
1863 // The "Beta" label is added separately for themes.
1864 $instance->is_plugin() &&
1865 $instance->is_beta()
1866 );
1867
1868 if ( ! $is_beta && ! $has_beta_update ) {
1869 continue;
1870 }
1871
1872 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1873
1874 if ( ! $has_beta_update ) {
1875 continue;
1876 }
1877
1878 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1879 '%s %s',
1880 sprintf(
1881 fs_esc_attr_inline(
1882 '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.',
1883 'beta-version-update-caution',
1884 $instance->get_slug()
1885 ),
1886 $instance->get_plugin_title()
1887 ),
1888 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1889 );
1890 }
1891
1892 if ( empty( $beta_data ) ) {
1893 return;
1894 }
1895 ?>
1896 <script type="text/javascript">
1897 ( function( $ ) {
1898 var betaData = <?php echo json_encode( $beta_data ) ?>;
1899
1900 for ( var pluginBasename in betaData ) {
1901 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1902 continue;
1903 }
1904
1905 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1906 continue;
1907 }
1908
1909 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1910 if ( 0 === $parentContainer.length ) {
1911 continue;
1912 }
1913
1914 $parentContainer.find( '.plugin-title > strong:first-child').append(
1915 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1916 );
1917 }
1918
1919 setTimeout( function() {
1920 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1921 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1922 var $parentContainer = $( this ).parents( 'tr:first' );
1923 pluginBasename = ( 0 !== $parentContainer.length ) ?
1924 $parentContainer.data( 'plugin' ) :
1925 $( this ).parents( '.theme:first' ).data( 'slug' );
1926
1927 if (
1928 betaData[ pluginBasename ] &&
1929 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1930 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1931 ) {
1932 return false;
1933 }
1934 } );
1935 }, 20 );
1936 } )( jQuery );
1937 </script>
1938 <?php
1939 }
1940
1941 /**
1942 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1943 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1944 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1945 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1946 *
1947 * @author Leo Fajardo (@leorw)
1948 *
1949 * @since 1.2.0
1950 */
1951 private function unregister_uninstall_hook() {
1952 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1953 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1954 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1955
1956 update_option( 'uninstall_plugins', $uninstallable_plugins );
1957 }
1958
1959 /**
1960 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1961 *
1962 * @param bool $store_prev_path
1963 */
1964 private function clear_module_main_file_cache( $store_prev_path = true ) {
1965 if ( ! isset( $this->_storage->plugin_main_file ) ||
1966 empty( $this->_storage->plugin_main_file->path )
1967 ) {
1968 return;
1969 }
1970
1971 if ( ! $store_prev_path ) {
1972 /**
1973 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1974 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1975 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1976 *
1977 * @author Leo Fajardo (@leorw)
1978 * @since 2.2.1
1979 */
1980 unset( $this->_storage->plugin_main_file->path );
1981 } else {
1982 $plugin_main_file = clone $this->_storage->plugin_main_file;
1983
1984 // Store cached path (2nd layer cache).
1985 $plugin_main_file->prev_path = $plugin_main_file->path;
1986
1987 // Clear cached path.
1988 unset( $plugin_main_file->path );
1989
1990 $this->_storage->plugin_main_file = $plugin_main_file;
1991 }
1992
1993 /**
1994 * Clear global cached path.
1995 *
1996 * @author Leo Fajardo (@leorw)
1997 * @since 1.2.2
1998 */
1999 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
2000 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
2001 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2002 }
2003
2004 /**
2005 * @author Leo Fajardo (@leorw)
2006 * @since 2.0.0
2007 */
2008 function _hook_action_links_and_register_account_hooks() {
2009 $this->_add_tracking_links();
2010
2011 if ( self::is_plugins_page() && $this->is_plugin() ) {
2012 $this->hook_plugin_action_links();
2013 }
2014
2015 $this->_register_account_hooks();
2016 }
2017
2018 /**
2019 * @author Vova Feldman (@svovaf)
2020 * @since 1.0.9
2021 */
2022 private function _register_account_hooks() {
2023 if ( ! is_admin() ) {
2024 return;
2025 }
2026
2027 /**
2028 * Always show the deactivation feedback form since we added
2029 * automatic free version deactivation upon premium code activation.
2030 *
2031 * @since 1.2.1.6
2032 */
2033 $this->add_ajax_action(
2034 'submit_uninstall_reason',
2035 array( &$this, '_submit_uninstall_reason_action' )
2036 );
2037
2038 $this->add_ajax_action(
2039 'cancel_subscription_or_trial',
2040 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
2041 );
2042
2043 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
2044 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
2045 ( $this->is_theme() && self::is_themes_page() )
2046 ) {
2047 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
2048 }
2049 }
2050 }
2051
2052 /**
2053 * Leverage backtrace to find caller plugin file path.
2054 *
2055 * @author Vova Feldman (@svovaf)
2056 * @since 1.0.6
2057 *
2058 * @param bool $is_init Is initiation sequence.
2059 *
2060 * @return string
2061 */
2062 private function _find_caller_plugin_file( $is_init = false ) {
2063 // Try to load the cached value of the file path.
2064 if ( isset( $this->_storage->plugin_main_file ) ) {
2065 $plugin_main_file = $this->_storage->plugin_main_file;
2066 if ( ! empty( $plugin_main_file->path ) ) {
2067 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
2068 if ( file_exists( $absolute_path ) ) {
2069 return $absolute_path;
2070 }
2071 }
2072 }
2073
2074 /**
2075 * @since 1.2.1
2076 *
2077 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
2078 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
2079 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
2080 *
2081 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
2082 * when the class instantiator isn't the module.
2083 */
2084 if ( ! $is_init ) {
2085 // Fetch prev path cache.
2086 if ( isset( $this->_storage->plugin_main_file ) &&
2087 ! empty( $this->_storage->plugin_main_file->prev_path )
2088 ) {
2089 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2090 if ( file_exists( $absolute_path ) ) {
2091 return $absolute_path;
2092 }
2093 }
2094
2095 wp_die(
2096 $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' ) .
2097 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2098 $this->get_text_inline( 'Error', 'error' ),
2099 array( 'back_link' => true )
2100 );
2101 }
2102
2103 /**
2104 * @since 1.2.1
2105 *
2106 * Only the original instantiator that calls dynamic_init can modify the module's path.
2107 */
2108 // Find caller module.
2109 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2110 $this->_storage->plugin_main_file = (object) array(
2111 'path' => $id_slug_type_path_map[ $this->_module_id ]['path'],
2112 );
2113
2114 return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
2115 }
2116
2117 /**
2118 * @author Leo Fajardo (@leorw)
2119 * @since 1.2.3
2120 *
2121 * @param string $path
2122 *
2123 * @return string
2124 */
2125 private function get_relative_path( $path ) {
2126 $module_root_dir = $this->get_module_root_dir_path();
2127 if ( 0 === strpos( $path, $module_root_dir ) ) {
2128 $path = substr( $path, strlen( $module_root_dir ) );
2129 }
2130
2131 return $path;
2132 }
2133
2134 /**
2135 * @author Leo Fajardo (@leorw)
2136 * @since 1.2.3
2137 *
2138 * @param string $path
2139 * @param string|bool $module_type
2140 *
2141 * @return string
2142 */
2143 private function get_absolute_path( $path, $module_type = false ) {
2144 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2145 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2146 $path = fs_normalize_path( $module_root_dir . $path );
2147 }
2148
2149 return $path;
2150 }
2151
2152 /**
2153 * @author Leo Fajardo (@leorw)
2154 * @since 1.2.3
2155 *
2156 * @param string|bool $module_type
2157 *
2158 * @return string
2159 */
2160 private function get_module_root_dir_path( $module_type = false ) {
2161 $is_plugin = empty( $module_type ) ?
2162 $this->is_plugin() :
2163 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2164
2165 return fs_normalize_path( trailingslashit( $is_plugin ?
2166 WP_PLUGIN_DIR :
2167 get_theme_root( get_stylesheet() ) ) );
2168 }
2169
2170 /**
2171 * @author Leo Fajardo (@leorw)
2172 *
2173 * @param number $module_id
2174 * @param string $slug
2175 *
2176 * @since 1.2.2
2177 */
2178 private function store_id_slug_type_path_map( $module_id, $slug ) {
2179 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2180
2181 $store_option = false;
2182
2183 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2184 $id_slug_type_path_map[ $module_id ] = array(
2185 'slug' => $slug
2186 );
2187
2188 $store_option = true;
2189 }
2190
2191 if ( empty( $id_slug_type_path_map[ $module_id ]['path'] ) ||
2192 /**
2193 * This verification is for cases when suddenly the same module
2194 * is installed but with a different folder name.
2195 *
2196 * @author Vova Feldman (@svovaf)
2197 * @since 1.2.3
2198 */
2199 ! file_exists( $this->get_absolute_path(
2200 $id_slug_type_path_map[ $module_id ]['path'],
2201 $id_slug_type_path_map[ $module_id ]['type']
2202 ) )
2203 ) {
2204 $caller_main_file_and_type = $this->get_caller_main_file_and_type();
2205
2206 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2207 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2208
2209 $store_option = true;
2210 }
2211
2212 if ( $store_option ) {
2213 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2214 }
2215 }
2216
2217 /**
2218 * Identifies the caller type: plugin or theme.
2219 *
2220 * @author Leo Fajardo (@leorw)
2221 * @since 1.2.2
2222 *
2223 * @author Vova Feldman (@svovaf)
2224 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2225 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2226 * SDK an internal file instead of directly from functions.php.
2227 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2228 */
2229 private function get_caller_main_file_and_type() {
2230 self::require_plugin_essentials();
2231
2232 $all_plugins = fs_get_plugins( true );
2233 $all_plugins_paths = array();
2234
2235 // Get active plugin's main files real full names (might be symlinks).
2236 foreach ( $all_plugins as $relative_path => $data ) {
2237 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2238 /**
2239 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2240 * can't really include the SDK.
2241 *
2242 * @author Vova Feldman
2243 * @since 1.2.1.7
2244 */
2245 continue;
2246 }
2247
2248 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2249 }
2250
2251 $caller_file_candidate = false;
2252 $caller_map = array();
2253 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2254 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2255 $plugin_dir_to_skip = false;
2256
2257 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2258 if ( empty( $bt[ $i ]['file'] ) ) {
2259 continue;
2260 }
2261
2262 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2263 // If file same as the prev file in the stack, skip it.
2264 continue;
2265 }
2266
2267 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2268 'do_action',
2269 'apply_filter',
2270 // The string split is stupid, but otherwise, theme check
2271 // throws info notices.
2272 'requir' . 'e_once',
2273 'requir' . 'e',
2274 'includ' . 'e_once',
2275 'includ' . 'e',
2276 'install_and_activate_plugin',
2277 'try_activate_plugin',
2278 'activate_plugin'
2279 ) )
2280 ) {
2281 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2282 /**
2283 * Store the directory of the activator plugin so that any other file that starts with it
2284 * cannot be mistakenly chosen as a candidate caller file.
2285 *
2286 * @author Leo Fajardo
2287 *
2288 * @since 2.3.0
2289 */
2290 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2291
2292 foreach ( $all_plugins_paths as $plugin_path ) {
2293 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2294 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2295 $plugin_dir_to_skip = $plugin_dir;
2296
2297 break;
2298 }
2299 }
2300 }
2301
2302 // Ignore call stack hooks and files inclusion.
2303 continue;
2304 }
2305
2306 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2307
2308 if ( ! empty( $plugin_dir_to_skip ) ) {
2309 /**
2310 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2311 *
2312 * @author Leo Fajardo
2313 *
2314 * @since 2.3.0
2315 */
2316 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2317 continue;
2318 }
2319 }
2320
2321 if ( 'functions.php' === basename( $caller_file_path ) ) {
2322 /**
2323 * 1. Assumes that theme's starting execution file is functions.php.
2324 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2325 *
2326 * @author Vova Feldman (@svovaf)
2327 * @since 1.2.2.5
2328 */
2329
2330 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2331 $module_type = WP_FS__MODULE_TYPE_THEME;
2332
2333 /**
2334 * Relative path of the theme, e.g.:
2335 * `my-theme/functions.php`
2336 *
2337 * @author Leo Fajardo (@leorw)
2338 */
2339 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2340 '/' .
2341 basename( $caller_file_path );
2342
2343 continue;
2344 }
2345 }
2346
2347 $caller_file_hash = md5( $caller_file_path );
2348
2349 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2350 foreach ( $all_plugins_paths as $plugin_path ) {
2351 if ( empty( $plugin_path ) ) {
2352 continue;
2353 }
2354
2355 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2356 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2357 break;
2358 }
2359 }
2360 }
2361
2362 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2363 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2364 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2365 }
2366 }
2367
2368 return (object) array(
2369 'module_type' => $module_type,
2370 'path' => $caller_file_candidate
2371 );
2372 }
2373
2374 #----------------------------------------------------------------------------------
2375 #region Deactivation Feedback Form
2376 #----------------------------------------------------------------------------------
2377
2378 /**
2379 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2380 * page.
2381 *
2382 * @author Vova Feldman (@svovaf)
2383 * @author Leo Fajardo (@leorw)
2384 *
2385 * @since 1.1.2
2386 */
2387 function _add_deactivation_feedback_dialog_box() {
2388 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2389 $this->_get_subscription_cancellation_dialog_box_template_params() :
2390 array();
2391
2392 /**
2393 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2394 */
2395 $show_deactivation_feedback_form = true;
2396 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2397 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2398 } else if ( $this->is_addon() ) {
2399 /**
2400 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2401 */
2402 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2403 }
2404
2405 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2406
2407 if (
2408 empty( $subscription_cancellation_dialog_box_template_params ) &&
2409 ! $show_deactivation_feedback_form &&
2410 empty( $uninstall_confirmation_message )
2411 ) {
2412 return;
2413 }
2414
2415 $vars = array( 'id' => $this->_module_id );
2416
2417 if ( $show_deactivation_feedback_form ) {
2418 /* Check the type of user:
2419 * 1. Long-term (long-term)
2420 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2421 * 3. Short-term (short-term)
2422 */
2423 $is_long_term_user = true;
2424
2425 // Check if the site is at least 2 days old.
2426 $time_installed = $this->_storage->install_timestamp;
2427
2428 // Difference in seconds.
2429 $date_diff = time() - $time_installed;
2430
2431 // Convert seconds to days.
2432 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2433
2434 if ( $date_diff_days < 2 ) {
2435 $is_long_term_user = false;
2436 }
2437
2438 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2439
2440 if ( $is_long_term_user ) {
2441 $user_type = 'long-term';
2442 } else {
2443 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2444 $user_type = 'non-registered-and-non-anonymous-short-term';
2445 } else {
2446 $user_type = 'short-term';
2447 }
2448 }
2449
2450 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2451
2452 $vars['reasons'] = $uninstall_reasons;
2453 }
2454
2455 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2456 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2457 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2458
2459 /**
2460 * Load the HTML template for the deactivation feedback dialog box.
2461 *
2462 * @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.
2463 */
2464 fs_require_template( 'forms/deactivation/form.php', $vars );
2465 }
2466
2467 /**
2468 * @author Leo Fajardo (@leorw)
2469 * @since 1.1.2
2470 *
2471 * @param string $user_type
2472 *
2473 * @return array The uninstall reasons for the specified user type.
2474 */
2475 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2476 $module_type = $this->_module_type;
2477
2478 $internal_message_template_var = array(
2479 'id' => $this->_module_id
2480 );
2481
2482 $plan = $this->get_plan();
2483
2484 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2485 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2486 } else {
2487 $contact_support_template = '';
2488 }
2489
2490 $reason_found_better_plugin = array(
2491 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2492 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2493 'input_type' => 'textfield',
2494 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2495 );
2496
2497 $reason_temporary_deactivation = array(
2498 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2499 'text' => sprintf(
2500 $this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ),
2501 strtolower( $this->is_plugin() ?
2502 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2503 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2504 )
2505 ),
2506 'input_type' => '',
2507 'input_placeholder' => ''
2508 );
2509
2510 $reason_other = array(
2511 'id' => self::REASON_OTHER,
2512 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2513 'input_type' => 'textfield',
2514 'input_placeholder' => ''
2515 );
2516
2517 $long_term_user_reasons = array(
2518 array(
2519 'id' => self::REASON_NO_LONGER_NEEDED,
2520 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2521 'input_type' => '',
2522 'input_placeholder' => ''
2523 ),
2524 $reason_found_better_plugin,
2525 array(
2526 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2527 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2528 'input_type' => '',
2529 'input_placeholder' => ''
2530 ),
2531 array(
2532 'id' => self::REASON_BROKE_MY_SITE,
2533 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2534 'input_type' => '',
2535 'input_placeholder' => '',
2536 'internal_message' => $contact_support_template
2537 ),
2538 array(
2539 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2540 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2541 'input_type' => '',
2542 'input_placeholder' => '',
2543 'internal_message' => $contact_support_template
2544 )
2545 );
2546
2547 if ( $this->is_paying() ) {
2548 $long_term_user_reasons[] = array(
2549 'id' => self::REASON_CANT_PAY_ANYMORE,
2550 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2551 'input_type' => 'textfield',
2552 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2553 );
2554 }
2555
2556 $reason_dont_share_info = array(
2557 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2558 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2559 'input_type' => '',
2560 'input_placeholder' => ''
2561 );
2562
2563 /**
2564 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2565 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2566 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2567 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2568 * button in the opt-in form is shown/hidden).
2569 */
2570 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2571 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2572 }
2573
2574 $uninstall_reasons = array(
2575 'long-term' => $long_term_user_reasons,
2576 'non-registered-and-non-anonymous-short-term' => array(
2577 array(
2578 'id' => self::REASON_DIDNT_WORK,
2579 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2580 'input_type' => '',
2581 'input_placeholder' => ''
2582 ),
2583 $reason_dont_share_info,
2584 $reason_found_better_plugin
2585 ),
2586 'short-term' => array(
2587 array(
2588 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2589 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2590 'input_type' => '',
2591 'input_placeholder' => '',
2592 'internal_message' => $contact_support_template
2593 ),
2594 $reason_found_better_plugin,
2595 array(
2596 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2597 '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 ),
2598 'input_type' => 'textarea',
2599 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2600 ),
2601 array(
2602 'id' => self::REASON_NOT_WORKING,
2603 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2604 'input_type' => 'textarea',
2605 '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' )
2606 ),
2607 array(
2608 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2609 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2610 'input_type' => 'textarea',
2611 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2612 ),
2613 array(
2614 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2615 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2616 'input_type' => 'textarea',
2617 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2618 )
2619 )
2620 );
2621
2622 // Randomize the reasons for the current user type.
2623 shuffle( $uninstall_reasons[ $user_type ] );
2624
2625 // Keep the following reasons as the last items in the list.
2626 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2627 $uninstall_reasons[ $user_type ][] = $reason_other;
2628
2629 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2630
2631 return $uninstall_reasons[ $user_type ];
2632 }
2633
2634 /**
2635 * Called after the user has submitted his reason for deactivating the plugin.
2636 *
2637 * @author Leo Fajardo (@leorw)
2638 * @since 1.1.2
2639 */
2640 function _submit_uninstall_reason_action() {
2641 $this->_logger->entrance();
2642
2643 $this->check_ajax_referer( 'submit_uninstall_reason' );
2644
2645 $reason_id = fs_request_get( 'reason_id' );
2646
2647 // Check if the given reason ID is an unsigned integer.
2648 if ( ! ctype_digit( $reason_id ) ) {
2649 exit;
2650 }
2651
2652 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2653 if ( ! empty( $reason_info ) ) {
2654 $reason_info = substr( $reason_info, 0, 128 );
2655 }
2656
2657 $reason = (object) array(
2658 'id' => $reason_id,
2659 'info' => $reason_info,
2660 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2661 );
2662
2663 $this->_storage->store( 'uninstall_reason', $reason );
2664
2665 /**
2666 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2667 * not support uninstall hook.
2668 *
2669 * @author Leo Fajardo (@leorw)
2670 * @since 1.2.2
2671 */
2672 if ( $this->is_theme() ) {
2673 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2674 FS_Plugin_Updater::instance( $this )->delete_update_data();
2675 }
2676
2677 $this->_uninstall_plugin_event( false );
2678 $this->remove_sdk_reference();
2679 }
2680
2681 // Print '1' for successful operation.
2682 echo 1;
2683 exit;
2684 }
2685
2686 /**
2687 * @author Leo Fajardo (@leorw)
2688 * @since 2.1.4
2689 */
2690 function cancel_subscription_or_trial_ajax_action() {
2691 $this->_logger->entrance();
2692
2693 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2694
2695 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2696
2697 if ( $this->is_api_error( $result ) ) {
2698 $this->shoot_ajax_failure( $result->error->message );
2699 }
2700
2701 $this->shoot_ajax_success();
2702 }
2703
2704 /**
2705 * @author Leo Fajardo (@leorw)
2706 * @since 2.1.4
2707 *
2708 * @param number $plugin_id
2709 *
2710 * @return object
2711 */
2712 private function cancel_subscription_or_trial( $plugin_id ) {
2713 $fs = null;
2714 if ( $plugin_id == $this->get_id() ) {
2715 $fs = $this;
2716 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2717 $fs = self::get_instance_by_id( $plugin_id );
2718 }
2719
2720 $result = null;
2721
2722 if ( ! is_null( $fs ) ) {
2723 $result = $fs->is_paid_trial() ?
2724 $fs->_cancel_trial() :
2725 $fs->_downgrade_site();
2726 }
2727
2728 return $result;
2729 }
2730
2731 /**
2732 * @author Leo Fajardo (@leorw)
2733 * @since 2.0.2
2734 */
2735 function _delete_theme_update_data_action() {
2736 FS_Plugin_Updater::instance( $this )->delete_update_data();
2737 }
2738
2739 #endregion
2740
2741 #----------------------------------------------------------------------------------
2742 #region Instance
2743 #----------------------------------------------------------------------------------
2744
2745 /**
2746 * Main singleton instance.
2747 *
2748 * @author Vova Feldman (@svovaf)
2749 * @since 1.0.0
2750 *
2751 * @param number $module_id
2752 * @param string|bool $slug
2753 * @param bool $is_init Is initiation sequence.
2754 *
2755 * @return Freemius|false
2756 */
2757 static function instance( $module_id, $slug = false, $is_init = false ) {
2758 if ( empty( $module_id ) ) {
2759 return false;
2760 }
2761
2762 /**
2763 * 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.
2764 */
2765 self::_load_required_static();
2766
2767 if ( ! is_numeric( $module_id ) ) {
2768 if ( ! $is_init && true === $slug ) {
2769 $is_init = true;
2770 }
2771
2772 $slug = $module_id;
2773
2774 $module = FS_Plugin_Manager::instance( $slug )->get();
2775
2776 if ( is_object( $module ) ) {
2777 $module_id = $module->id;
2778 }
2779 }
2780
2781 $key = 'm_' . $module_id;
2782
2783 if ( ! isset( self::$_instances[ $key ] ) ) {
2784 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2785 }
2786
2787 return self::$_instances[ $key ];
2788 }
2789
2790 /**
2791 * @author Vova Feldman (@svovaf)
2792 * @since 1.0.6
2793 *
2794 * @param number $addon_id
2795 *
2796 * @return bool
2797 */
2798 private static function has_instance( $addon_id ) {
2799 return isset( self::$_instances[ 'm_' . $addon_id ] );
2800 }
2801
2802 /**
2803 * @author Leo Fajardo (@leorw)
2804 * @since 1.2.2
2805 *
2806 * @param string|number $id_or_slug
2807 * @param string $module_type
2808 *
2809 * @return number|false
2810 */
2811 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2812 if ( is_numeric( $id_or_slug ) ) {
2813 return $id_or_slug;
2814 }
2815
2816 foreach ( self::$_instances as $instance ) {
2817 // Also check the module type since there can be a plugin and a theme with the same slug.
2818 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2819 return $instance->get_id();
2820 }
2821 }
2822
2823 return false;
2824 }
2825
2826 /**
2827 * @author Vova Feldman (@svovaf)
2828 * @since 1.0.6
2829 *
2830 * @param number $id
2831 *
2832 * @return false|Freemius
2833 */
2834 static function get_instance_by_id( $id ) {
2835 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2836 self::$_instances[ 'm_' . $id ] :
2837 false;
2838 }
2839
2840 /**
2841 *
2842 * @author Vova Feldman (@svovaf)
2843 * @since 1.0.1
2844 *
2845 * @param string $plugin_file
2846 * @param string $module_type
2847 *
2848 * @return false|Freemius
2849 */
2850 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2851 $slug = self::find_slug_by_basename( $plugin_file );
2852
2853 return ( false !== $slug ) ?
2854 self::instance( self::get_module_id( $slug, $module_type ) ) :
2855 false;
2856 }
2857
2858 /**
2859 * @author Vova Feldman (@svovaf)
2860 * @since 1.0.6
2861 *
2862 * @return false|Freemius
2863 */
2864 function get_parent_instance() {
2865 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2866 }
2867
2868 /**
2869 * @author Vova Feldman (@svovaf)
2870 * @since 1.0.6
2871 *
2872 * @param string|number $id_or_slug
2873 *
2874 * @return false|Freemius
2875 */
2876 function get_addon_instance( $id_or_slug ) {
2877 $addon_id = self::get_module_id( $id_or_slug );
2878
2879 return self::instance( $addon_id );
2880 }
2881
2882 #endregion ------------------------------------------------------------------
2883
2884 /**
2885 * @author Vova Feldman (@svovaf)
2886 * @since 1.0.6
2887 *
2888 * @return bool
2889 */
2890 function is_parent_plugin_installed() {
2891 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2892
2893 if ( $is_active ) {
2894 return true;
2895 }
2896
2897 /**
2898 * Parent module might be a theme. If that's the case, the add-on's FS
2899 * instance will be loaded prior to the theme's FS instance, therefore,
2900 * we need to check if it's active with a "look ahead".
2901 *
2902 * @author Vova Feldman
2903 * @since 1.2.2.3
2904 */
2905 global $fs_active_plugins;
2906 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2907 $active_theme = wp_get_theme();
2908
2909 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2910 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2911 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2912 // Parent module is a theme and it's currently active.
2913 return true;
2914 }
2915 }
2916 }
2917 }
2918
2919 return false;
2920 }
2921
2922 /**
2923 * Check if add-on parent plugin in activation mode.
2924 *
2925 * @author Vova Feldman (@svovaf)
2926 * @since 1.0.7
2927 *
2928 * @return bool
2929 */
2930 function is_parent_in_activation() {
2931 $parent_fs = $this->get_parent_instance();
2932 if ( ! is_object( $parent_fs ) ) {
2933 return false;
2934 }
2935
2936 return ( $parent_fs->is_activation_mode() );
2937 }
2938
2939 /**
2940 * Is plugin in activation mode.
2941 *
2942 * @author Vova Feldman (@svovaf)
2943 * @since 1.0.7
2944 *
2945 * @param bool $and_on
2946 *
2947 * @return bool
2948 */
2949 function is_activation_mode( $and_on = true ) {
2950 return fs_is_network_admin() ?
2951 $this->is_network_activation_mode( $and_on ) :
2952 $this->is_site_activation_mode( $and_on );
2953 }
2954
2955 /**
2956 * Is plugin in activation mode.
2957 *
2958 * @author Vova Feldman (@svovaf)
2959 * @since 1.0.7
2960 *
2961 * @param bool $and_on
2962 *
2963 * @return bool
2964 */
2965 function is_site_activation_mode( $and_on = true ) {
2966 return (
2967 ( $this->is_on() || ! $and_on ) &&
2968 (
2969 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
2970 (
2971 ( ! $this->is_registered() ||
2972 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
2973 ( ! $this->is_enable_anonymous() ||
2974 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
2975 )
2976 )
2977 );
2978 }
2979
2980 /**
2981 * Checks if the SDK in network activation mode.
2982 *
2983 * @author Leo Fajardo (@leorw)
2984 * @since 2.0.0
2985 *
2986 * @param bool $and_on
2987 *
2988 * @return bool
2989 */
2990 private function is_network_activation_mode( $and_on = true ) {
2991 if ( ! $this->_is_network_active ) {
2992 // Not network activated.
2993 return false;
2994 }
2995
2996 if ( $this->is_network_upgrade_mode() ) {
2997 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
2998 return true;
2999 }
3000
3001 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3002 // 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.
3003 return false;
3004 }
3005
3006 if ( $this->is_network_delegated_connection() ) {
3007 // Super-admin delegated the connection to the site admins -> not activation mode.
3008 return false;
3009 }
3010
3011 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3012 // Super-admin skipped the connection network wide -> not activation mode.
3013 return false;
3014 }
3015
3016 if ( $this->is_network_registered() ) {
3017 // Super-admin connected at least one site -> not activation mode.
3018 return false;
3019 }
3020
3021 return true;
3022 }
3023
3024 /**
3025 * Check if current page is the opt-in/pending-activation page.
3026 *
3027 * @author Vova Feldman (@svovaf)
3028 * @since 1.2.1.7
3029 *
3030 * @return bool
3031 */
3032 function is_activation_page() {
3033 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3034 return true;
3035 }
3036
3037 if ( ! $this->is_activation_mode() ) {
3038 return false;
3039 }
3040
3041 // Check if current page is matching the activation page.
3042 return $this->is_matching_url( $this->get_activation_url() );
3043 }
3044
3045 /**
3046 * Check if URL path's are matching and that all querystring
3047 * arguments of the $sub_url exist in the $url with the same values.
3048 *
3049 * WARNING:
3050 * 1. This method doesn't check if the sub/domain are matching.
3051 * 2. Ignore case sensitivity.
3052 *
3053 * @author Vova Feldman (@svovaf)
3054 * @since 1.2.1.7
3055 *
3056 * @param string $sub_url
3057 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3058 *
3059 * @return bool
3060 */
3061 private function is_matching_url( $sub_url, $url = '' ) {
3062 if ( empty( $url ) ) {
3063 $url = $_SERVER['REQUEST_URI'];
3064 }
3065
3066 $url = strtolower( $url );
3067 $sub_url = strtolower( $sub_url );
3068
3069 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3070 // Different path - DO NOT OVERRIDE PAGE.
3071 return false;
3072 }
3073
3074 $url_params = array();
3075 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
3076
3077 $sub_url_params = array();
3078 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
3079
3080 foreach ( $sub_url_params as $key => $val ) {
3081 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3082 // Not matching query string - DO NOT OVERRIDE PAGE.
3083 return false;
3084 }
3085 }
3086
3087 return true;
3088 }
3089
3090 /**
3091 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3092 *
3093 * @author Vova Feldman (@svovaf)
3094 * @since 2.0.0
3095 *
3096 * @param int $blog_id
3097 *
3098 * @return string[]
3099 */
3100 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3101 if ( is_multisite() && $blog_id > 0 ) {
3102 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3103 } else {
3104 $active_basenames = get_option( 'active_plugins' );
3105 }
3106
3107 if ( ! is_array( $active_basenames ) ) {
3108 $active_basenames = array();
3109 }
3110
3111 if ( is_multisite() ) {
3112 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3113
3114 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3115 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3116 }
3117 }
3118
3119 return $active_basenames;
3120 }
3121
3122 /**
3123 * @author Leo Fajardo (@leorw)
3124 * @since 2.3.0
3125 *
3126 * @param int $blog_id
3127 *
3128 * @return array
3129 */
3130 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3131 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3132
3133 $map = array();
3134
3135 foreach ( $active_basenames as $active_basename ) {
3136 $active_basename = fs_normalize_path( $active_basename );
3137
3138 if ( false === strpos( $active_basename, '/' ) ) {
3139 continue;
3140 }
3141
3142 $map[ dirname( $active_basename ) ] = true;
3143 }
3144
3145 return $map;
3146 }
3147
3148 /**
3149 * Get collection of all active plugins. Including network activated plugins.
3150 *
3151 * @author Vova Feldman (@svovaf)
3152 * @since 1.0.9
3153 *
3154 * @param int $blog_id Since 2.0.0
3155 *
3156 * @return array[string]array
3157 */
3158 private static function get_active_plugins( $blog_id = 0 ) {
3159 self::require_plugin_essentials();
3160
3161 $active_plugin = array();
3162 $all_plugins = fs_get_plugins();
3163 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3164
3165 foreach ( $active_plugins_basenames as $plugin_basename ) {
3166 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3167 }
3168
3169 return $active_plugin;
3170 }
3171
3172 /**
3173 * Get collection of all site active plugins for a specified blog.
3174 *
3175 * @author Vova Feldman (@svovaf)
3176 * @since 2.0.0
3177 *
3178 * @param int $blog_id
3179 *
3180 * @return array[string]array
3181 */
3182 private static function get_site_active_plugins( $blog_id = 0 ) {
3183 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3184 get_blog_option( $blog_id, 'active_plugins' ) :
3185 get_option( 'active_plugins' );
3186
3187 $active = array();
3188
3189 if ( ! is_array( $active_basenames ) ) {
3190 return $active;
3191 }
3192
3193 foreach ( $active_basenames as $basename ) {
3194 $active[ $basename ] = array(
3195 'is_active' => true,
3196 'Version' => '1.0', // Dummy version.
3197 'slug' => self::get_plugin_slug( $basename ),
3198 );
3199 }
3200
3201 return $active;
3202 }
3203
3204 /**
3205 * Get collection of all plugins with their activation status for a specified blog.
3206 *
3207 * @author Vova Feldman (@svovaf)
3208 * @since 1.1.8
3209 *
3210 * @param int $blog_id Since 2.0.0
3211 *
3212 * @return array Key is the plugin file path and the value is an array of the plugin data.
3213 */
3214 private static function get_all_plugins( $blog_id = 0 ) {
3215 self::require_plugin_essentials();
3216
3217 $all_plugins = fs_get_plugins();
3218
3219 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3220
3221 foreach ( $all_plugins as $basename => &$data ) {
3222 // By default set to inactive (next foreach update the active plugins).
3223 $data['is_active'] = false;
3224 // Enrich with plugin slug.
3225 $data['slug'] = self::get_plugin_slug( $basename );
3226 }
3227
3228 // Flag active plugins.
3229 foreach ( $active_plugins_basenames as $basename ) {
3230 if ( isset( $all_plugins[ $basename ] ) ) {
3231 $all_plugins[ $basename ]['is_active'] = true;
3232 }
3233 }
3234
3235 return $all_plugins;
3236 }
3237
3238 /**
3239 * Get collection of all plugins and if they are network level activated.
3240 *
3241 * @author Vova Feldman (@svovaf)
3242 * @since 2.0.0
3243 *
3244 * @return array Key is the plugin basename and the value is an array of the plugin data.
3245 */
3246 private static function get_network_plugins() {
3247 self::require_plugin_essentials();
3248
3249 $all_plugins = fs_get_plugins();
3250
3251 $network_active_basenames = is_multisite() ?
3252 get_site_option( 'active_sitewide_plugins' ) :
3253 array();
3254
3255 foreach ( $all_plugins as $basename => &$data ) {
3256 // By default set to inactive (next foreach update the active plugins).
3257 $data['is_active'] = false;
3258 // Enrich with plugin slug.
3259 $data['slug'] = self::get_plugin_slug( $basename );
3260 }
3261
3262 // Flag active plugins.
3263 foreach ( $network_active_basenames as $basename ) {
3264 if ( isset( $all_plugins[ $basename ] ) ) {
3265 $all_plugins[ $basename ]['is_active'] = true;
3266 }
3267 }
3268
3269 return $all_plugins;
3270 }
3271
3272 /**
3273 * Cached result of get_site_transient( 'update_plugins' )
3274 *
3275 * @author Vova Feldman (@svovaf)
3276 * @since 1.1.8
3277 *
3278 * @var object
3279 */
3280 private static $_plugins_info;
3281
3282 /**
3283 * Helper function to get specified plugin's slug.
3284 *
3285 * @author Vova Feldman (@svovaf)
3286 * @since 1.1.8
3287 *
3288 * @param $basename
3289 *
3290 * @return string
3291 */
3292 private static function get_plugin_slug( $basename ) {
3293 if ( ! isset( self::$_plugins_info ) ) {
3294 self::$_plugins_info = get_site_transient( 'update_plugins' );
3295 }
3296
3297 $slug = '';
3298
3299 if ( is_object( self::$_plugins_info ) ) {
3300 if ( isset( self::$_plugins_info->no_update ) &&
3301 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3302 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3303 ) {
3304 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3305 } else if ( isset( self::$_plugins_info->response ) &&
3306 isset( self::$_plugins_info->response[ $basename ] ) &&
3307 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3308 ) {
3309 $slug = self::$_plugins_info->response[ $basename ]->slug;
3310 }
3311 }
3312
3313 if ( empty( $slug ) ) {
3314 // Try to find slug from FS data.
3315 $slug = self::find_slug_by_basename( $basename );
3316 }
3317
3318 if ( empty( $slug ) ) {
3319 // Fallback to plugin's folder name.
3320 $slug = dirname( $basename );
3321 }
3322
3323 return $slug;
3324 }
3325
3326 private static $_statics_loaded = false;
3327
3328 /**
3329 * Load static resources.
3330 *
3331 * @author Vova Feldman (@svovaf)
3332 * @since 1.0.1
3333 */
3334 private static function _load_required_static() {
3335 if ( self::$_statics_loaded ) {
3336 return;
3337 }
3338
3339 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3340
3341 self::$_static_logger->entrance();
3342
3343 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3344
3345 if ( is_multisite() ) {
3346 $has_skipped_migration = (
3347 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3348 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3349 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3350 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3351 );
3352
3353 /**
3354 * If the file_slug_map exists on the site level but doesn't exist on the
3355 * network level storage, it means that we need to process the storage with migration.
3356 *
3357 * 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.
3358 *
3359 * @author Vova Feldman (@svovaf)
3360 * @since 2.0.0
3361 */
3362 if (
3363 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3364 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3365 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3366 ) {
3367 self::migrate_options_to_network();
3368 }
3369 }
3370
3371 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3372
3373 if ( ! WP_FS__DEMO_MODE ) {
3374 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3375 'Freemius',
3376 '_add_debug_section'
3377 ) );
3378 }
3379
3380 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3381
3382 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3383
3384 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3385
3386 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3387
3388 if ( 0 == did_action( 'plugins_loaded' ) ) {
3389 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3390 }
3391
3392 add_action( 'admin_footer', array( 'Freemius', '_enrich_ajax_url' ) );
3393 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3394
3395 if ( self::is_plugins_page() || self::is_themes_page() ) {
3396 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3397
3398 /**
3399 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3400 * page.
3401 *
3402 * @author Leo Fajardo (@leorw)
3403 * @since 2.3.0
3404 */
3405 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3406 }
3407
3408 self::$_statics_loaded = true;
3409 }
3410
3411 /**
3412 * @author Leo Fajardo (@leorw)
3413 *
3414 * @since 2.1.3
3415 */
3416 private static function migrate_options_to_network() {
3417 self::migrate_accounts_to_network();
3418
3419 // Migrate API options from site level to network level.
3420 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3421 $api_network_options->migrate_to_network();
3422
3423 // Migrate API cache to network level storage.
3424 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3425
3426 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3427 }
3428
3429 #----------------------------------------------------------------------------------
3430 #region Localization
3431 #----------------------------------------------------------------------------------
3432
3433 /**
3434 * Load framework's text domain.
3435 *
3436 * @author Vova Feldman (@svovaf)
3437 * @since 1.2.1
3438 */
3439 static function _load_textdomain() {
3440 if ( ! is_admin() ) {
3441 return;
3442 }
3443
3444 global $fs_active_plugins;
3445
3446 // Works both for plugins and themes.
3447 load_plugin_textdomain(
3448 'freemius',
3449 false,
3450 $fs_active_plugins->newest->sdk_path . '/languages/'
3451 );
3452 }
3453
3454 #endregion
3455
3456 #----------------------------------------------------------------------------------
3457 #region Debugging
3458 #----------------------------------------------------------------------------------
3459
3460 /**
3461 * @author Vova Feldman (@svovaf)
3462 * @since 1.0.8
3463 */
3464 static function _add_debug_section() {
3465 if ( ! is_super_admin() ) {
3466 // Add debug page only for super-admins.
3467 return;
3468 }
3469
3470 self::$_static_logger->entrance();
3471
3472 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3473
3474 if ( WP_FS__DEV_MODE ) {
3475 // Add top-level debug menu item.
3476 $hook = FS_Admin_Menu_Manager::add_page(
3477 $title,
3478 $title,
3479 'manage_options',
3480 'freemius',
3481 array( 'Freemius', '_debug_page_render' )
3482 );
3483 } else {
3484 // Add hidden debug page.
3485 $hook = FS_Admin_Menu_Manager::add_subpage(
3486 null,
3487 $title,
3488 $title,
3489 'manage_options',
3490 'freemius',
3491 array( 'Freemius', '_debug_page_render' )
3492 );
3493 }
3494
3495 if ( ! empty( $hook ) ) {
3496 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3497 }
3498 }
3499
3500 /**
3501 * @author Vova Feldman (@svovaf)
3502 * @since 1.1.7.3
3503 */
3504 static function _toggle_debug_mode() {
3505 if ( ! is_super_admin() ) {
3506 return;
3507 }
3508
3509 $is_on = fs_request_get( 'is_on', false, 'post' );
3510
3511 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3512 update_option( 'fs_debug_mode', $is_on );
3513
3514 // Turn on/off storage logging.
3515 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3516 }
3517
3518 exit;
3519 }
3520
3521 /**
3522 * @author Vova Feldman (@svovaf)
3523 * @since 1.2.1.6
3524 */
3525 static function _get_debug_log() {
3526 $logs = FS_Logger::load_db_logs(
3527 fs_request_get( 'filters', false, 'post' ),
3528 ! empty( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? $_POST['limit'] : 200,
3529 ! empty( $_POST['offset'] ) && is_numeric( $_POST['offset'] ) ? $_POST['offset'] : 0
3530 );
3531
3532 self::shoot_ajax_success( $logs );
3533 }
3534
3535 /**
3536 * @author Vova Feldman (@svovaf)
3537 * @since 1.2.1.7
3538 */
3539 static function _get_db_option() {
3540 check_admin_referer( 'fs_get_db_option' );
3541
3542 $option_name = fs_request_get( 'option_name' );
3543
3544 if ( ! is_super_admin() ||
3545 ! fs_starts_with( $option_name, 'fs_' )
3546 ) {
3547 self::shoot_ajax_failure();
3548 }
3549
3550 $value = get_option( $option_name );
3551
3552 $result = array(
3553 'name' => $option_name,
3554 );
3555
3556 if ( false !== $value ) {
3557 if ( ! is_string( $value ) ) {
3558 $value = json_encode( $value );
3559 }
3560
3561 $result['value'] = $value;
3562 }
3563
3564 self::shoot_ajax_success( $result );
3565 }
3566
3567 /**
3568 * @author Vova Feldman (@svovaf)
3569 * @since 1.2.1.7
3570 */
3571 static function _set_db_option() {
3572 check_admin_referer( 'fs_set_db_option' );
3573
3574 $option_name = fs_request_get( 'option_name' );
3575
3576 if ( ! is_super_admin() ||
3577 ! fs_starts_with( $option_name, 'fs_' )
3578 ) {
3579 self::shoot_ajax_failure();
3580 }
3581
3582 $option_value = fs_request_get( 'option_value' );
3583
3584 if ( ! empty( $option_value ) ) {
3585 update_option( $option_name, $option_value );
3586 }
3587
3588 self::shoot_ajax_success();
3589 }
3590
3591 /**
3592 * @author Vova Feldman (@svovaf)
3593 * @since 1.0.8
3594 */
3595 static function _debug_page_actions() {
3596 self::_clean_admin_content_section();
3597
3598 if ( fs_request_is_action( 'restart_freemius' ) ) {
3599 check_admin_referer( 'restart_freemius' );
3600
3601 if ( ! is_multisite() ) {
3602 // Clear accounts data.
3603 self::$_accounts->clear( null, true );
3604 } else {
3605 $sites = self::get_sites();
3606 foreach ( $sites as $site ) {
3607 $blog_id = self::get_site_blog_id( $site );
3608 self::$_accounts->clear( $blog_id, true );
3609 }
3610
3611 // Clear network level storage.
3612 self::$_accounts->clear( true, true );
3613 }
3614
3615 // Clear SDK reference cache.
3616 delete_option( 'fs_active_plugins' );
3617 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3618 check_admin_referer( 'clear_updates_data' );
3619
3620 if ( ! is_multisite() ) {
3621 set_site_transient( 'update_plugins', null );
3622 set_site_transient( 'update_themes', null );
3623 } else {
3624 $current_blog_id = get_current_blog_id();
3625
3626 $sites = self::get_sites();
3627 foreach ( $sites as $site ) {
3628 switch_to_blog( self::get_site_blog_id( $site ) );
3629
3630 set_site_transient( 'update_plugins', null );
3631 set_site_transient( 'update_themes', null );
3632 }
3633
3634 switch_to_blog( $current_blog_id );
3635 }
3636 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3637 check_admin_referer( 'simulate_trial' );
3638
3639 $fs = freemius( fs_request_get( 'module_id' ) );
3640
3641 // Update SDK install to at least 24 hours before.
3642 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3643 // Unset the trial shown timestamp.
3644 unset( $fs->_storage->trial_promotion_shown );
3645 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3646 check_admin_referer( 'simulate_network_upgrade' );
3647
3648 $fs = freemius( fs_request_get( 'module_id' ) );
3649
3650 self::set_network_upgrade_mode( $fs->_storage );
3651 } else if ( fs_request_is_action( 'delete_install' ) ) {
3652 check_admin_referer( 'delete_install' );
3653
3654 self::_delete_site_by_slug(
3655 fs_request_get( 'slug' ),
3656 fs_request_get( 'module_type' ),
3657 true,
3658 fs_request_get( 'blog_id', null )
3659 );
3660 } else if ( fs_request_is_action( 'delete_user' ) ) {
3661 check_admin_referer( 'delete_user' );
3662
3663 self::delete_user( fs_request_get( 'user_id' ) );
3664 } else if ( fs_request_is_action( 'download_logs' ) ) {
3665 check_admin_referer( 'download_logs' );
3666
3667 $download_url = FS_Logger::download_db_logs(
3668 fs_request_get( 'filters', false, 'post' )
3669 );
3670
3671 if ( false === $download_url ) {
3672 wp_die( 'Oops... there was an error while generating the logs download file. Please try again and if it doesn\'t work contact support@freemius.com.' );
3673 }
3674
3675 fs_redirect( $download_url );
3676 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3677 check_admin_referer( 'migrate_options_to_network' );
3678
3679 self::migrate_options_to_network();
3680 }
3681 }
3682
3683 /**
3684 * @author Vova Feldman (@svovaf)
3685 * @since 1.0.8
3686 */
3687 static function _debug_page_render() {
3688 self::$_static_logger->entrance();
3689
3690 if ( ! is_multisite() ) {
3691 $all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
3692 $all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME );
3693 } else {
3694 $sites = self::get_sites();
3695
3696 $all_plugins_installs = array();
3697 $all_themes_installs = array();
3698
3699 foreach ( $sites as $site ) {
3700 $blog_id = self::get_site_blog_id( $site );
3701
3702 $plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
3703
3704 foreach ( $plugins_installs as $slug => $install ) {
3705 if ( ! isset( $all_plugins_installs[ $slug ] ) ) {
3706 $all_plugins_installs[ $slug ] = array();
3707 }
3708
3709 $install->blog_id = $blog_id;
3710
3711 $all_plugins_installs[ $slug ][] = $install;
3712 }
3713
3714 $themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id );
3715
3716 foreach ( $themes_installs as $slug => $install ) {
3717 if ( ! isset( $all_themes_installs[ $slug ] ) ) {
3718 $all_themes_installs[ $slug ] = array();
3719 }
3720
3721 $install->blog_id = $blog_id;
3722
3723 $all_themes_installs[ $slug ][] = $install;
3724 }
3725 }
3726 }
3727
3728 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3729
3730 $vars = array(
3731 'plugin_sites' => $all_plugins_installs,
3732 'theme_sites' => $all_themes_installs,
3733 'users' => self::get_all_users(),
3734 'addons' => self::get_all_addons(),
3735 'account_addons' => self::get_all_account_addons(),
3736 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3737 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3738 );
3739
3740 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3741 fs_require_once_template( 'debug.php', $vars );
3742 }
3743
3744 #endregion
3745
3746 #----------------------------------------------------------------------------------
3747 #region Connectivity Issues
3748 #----------------------------------------------------------------------------------
3749
3750 /**
3751 * Check if Freemius should be turned on for the current plugin install.
3752 *
3753 * Note:
3754 * $this->_is_on is updated in has_api_connectivity()
3755 *
3756 * @author Vova Feldman (@svovaf)
3757 * @since 1.0.9
3758 *
3759 * @return bool
3760 */
3761 function is_on() {
3762 self::$_static_logger->entrance();
3763
3764 if ( isset( $this->_is_on ) ) {
3765 return $this->_is_on;
3766 }
3767
3768 // If already installed or pending then sure it's on :)
3769 if ( $this->is_registered() || $this->is_pending_activation() ) {
3770 $this->_is_on = true;
3771
3772 return true;
3773 }
3774
3775 return false;
3776 }
3777
3778 /**
3779 * @author Vova Feldman (@svovaf)
3780 * @since 1.1.7.3
3781 *
3782 * @param bool $flush_if_no_connectivity
3783 *
3784 * @return bool
3785 */
3786 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3787 if ( ! isset( $this->_storage->connectivity_test ) ) {
3788 // Connectivity test was never executed, or cache was cleared.
3789 return true;
3790 }
3791
3792 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3793 if ( WP_FS__IS_HTTP_REQUEST ) {
3794 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3795 // Domain changed.
3796 return true;
3797 }
3798
3799 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3800 // Server IP changed.
3801 return true;
3802 }
3803 }
3804 }
3805
3806 if ( $this->_storage->connectivity_test['is_connected'] &&
3807 $this->_storage->connectivity_test['is_active']
3808 ) {
3809 // API connected and Freemius is active - no need to run connectivity check.
3810 return false;
3811 }
3812
3813 if ( $flush_if_no_connectivity ) {
3814 /**
3815 * If explicitly asked to flush when no connectivity - do it only
3816 * if at least 10 sec passed from the last API connectivity test.
3817 */
3818 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3819 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3820 }
3821
3822 /**
3823 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3824 */
3825 $version = $this->get_plugin_version();
3826 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3827 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3828 return true;
3829 }
3830
3831 return false;
3832 }
3833
3834 /**
3835 * @author Vova Feldman (@svovaf)
3836 * @since 1.1.7.4
3837 *
3838 * @param int|null $blog_id Since 2.0.0.
3839 * @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test.
3840 *
3841 * @return object|false
3842 */
3843 private function ping( $blog_id = null, $is_gdpr_test = false ) {
3844 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
3845 return false;
3846 }
3847
3848 $version = $this->get_plugin_version();
3849
3850 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
3851
3852 return $this->get_api_plugin_scope()->ping(
3853 $this->get_anonymous_id( $blog_id ),
3854 array(
3855 'is_update' => json_encode( $is_update ),
3856 'version' => $version,
3857 'sdk' => $this->version,
3858 'is_admin' => json_encode( is_admin() ),
3859 'is_ajax' => json_encode( self::is_ajax() ),
3860 'is_cron' => json_encode( self::is_cron() ),
3861 'is_gdpr_test' => $is_gdpr_test,
3862 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
3863 )
3864 );
3865 }
3866
3867 /**
3868 * Check if there's any connectivity issue to Freemius API.
3869 *
3870 * @author Vova Feldman (@svovaf)
3871 * @since 1.0.9
3872 *
3873 * @param bool $flush_if_no_connectivity
3874 *
3875 * @return bool
3876 */
3877 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3878 $this->_logger->entrance();
3879
3880 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3881 return $this->_has_api_connection;
3882 }
3883
3884 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3885 isset( $this->_storage->connectivity_test ) &&
3886 true === $this->_storage->connectivity_test['is_connected']
3887 ) {
3888 unset( $this->_storage->connectivity_test );
3889 }
3890
3891 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3892 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3893 /**
3894 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3895 *
3896 * @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.
3897 */
3898 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3899 $this->is_premium() ||
3900 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3901
3902 return $this->_has_api_connection;
3903 }
3904
3905 $pong = $this->ping();
3906 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3907
3908 if ( ! $is_connected ) {
3909 // API failure.
3910 $this->_add_connectivity_issue_message( $pong );
3911 }
3912
3913 if ( $is_connected ) {
3914 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
3915 }
3916
3917 $this->store_connectivity_info( $pong, $is_connected );
3918
3919 return $this->_has_api_connection;
3920 }
3921
3922 /**
3923 * @author Vova Feldman (@svovaf)
3924 * @since 1.1.7.4
3925 *
3926 * @param object $pong
3927 * @param bool $is_connected
3928 */
3929 private function store_connectivity_info( $pong, $is_connected ) {
3930 $this->_logger->entrance();
3931
3932 $version = $this->get_plugin_version();
3933
3934 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3935 $is_active = false;
3936 } else {
3937 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3938 }
3939
3940 $is_active = $this->apply_filters(
3941 'is_on',
3942 $is_active,
3943 $this->is_plugin_update(),
3944 $version
3945 );
3946
3947 $this->_storage->connectivity_test = array(
3948 'is_connected' => $is_connected,
3949 'host' => $_SERVER['HTTP_HOST'],
3950 'server_ip' => WP_FS__REMOTE_ADDR,
3951 'is_active' => $is_active,
3952 'timestamp' => WP_FS__SCRIPT_START_TIME,
3953 // Last version with connectivity attempt.
3954 'version' => $version,
3955 );
3956
3957 $this->_has_api_connection = $is_connected;
3958 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3959 }
3960
3961 /**
3962 * Force turning Freemius on.
3963 *
3964 * @author Vova Feldman (@svovaf)
3965 * @since 1.1.8.1
3966 *
3967 * @return bool TRUE if successfully turned on.
3968 */
3969 private function turn_on() {
3970 $this->_logger->entrance();
3971
3972 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3973 return false;
3974 }
3975
3976 $updated_connectivity = $this->_storage->connectivity_test;
3977 $updated_connectivity['is_active'] = true;
3978 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3979 $this->_storage->connectivity_test = $updated_connectivity;
3980
3981 $this->_is_on = true;
3982
3983 return true;
3984 }
3985
3986 /**
3987 * Anonymous and unique site identifier (Hash).
3988 *
3989 * @author Vova Feldman (@svovaf)
3990 * @since 1.1.0
3991 *
3992 * @param null|int $blog_id Since 2.0.0
3993 *
3994 * @return string
3995 */
3996 function get_anonymous_id( $blog_id = null ) {
3997 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
3998
3999 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4000 $key = fs_strip_url_protocol( get_site_url( $blog_id ) );
4001
4002 $secure_auth = SECURE_AUTH_KEY;
4003 if ( empty( $secure_auth ) ||
4004 false !== strpos( $secure_auth, ' ' ) ||
4005 'put your unique phrase here' === $secure_auth
4006 ) {
4007 // Protect against default auth key.
4008 $secure_auth = md5( microtime() );
4009 }
4010
4011 /**
4012 * Base the unique identifier on the WP secure authentication key. Which
4013 * turns the key into a secret anonymous identifier. This will help us
4014 * to avoid duplicate installs generation on the backend upon opt-in.
4015 *
4016 * @author Vova Feldman (@svovaf)
4017 * @since 1.2.3
4018 */
4019 $unique_id = md5( $key . $secure_auth );
4020
4021 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4022 }
4023
4024 $this->_logger->departure( $unique_id );
4025
4026 return $unique_id;
4027 }
4028
4029 /**
4030 * @author Vova Feldman (@svovaf)
4031 * @since 1.1.7.4
4032 *
4033 * @return \WP_User
4034 */
4035 static function _get_current_wp_user() {
4036 self::require_pluggable_essentials();
4037 self::wp_cookie_constants();
4038
4039 return wp_get_current_user();
4040 }
4041
4042 /**
4043 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4044 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4045 * is network activated the cookie constants are only configured after the network
4046 * plugins activation, therefore, if we don't define those constants WP will throw
4047 * PHP warnings/notices.
4048 *
4049 * @author Vova Feldman (@svovaf)
4050 * @since 2.1.1
4051 */
4052 private static function wp_cookie_constants() {
4053 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4054 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4055 ) {
4056 return;
4057 }
4058
4059 /**
4060 * Used to guarantee unique hash cookies
4061 *
4062 * @since 1.5.0
4063 */
4064 if ( ! defined( 'COOKIEHASH' ) ) {
4065 $siteurl = get_site_option( 'siteurl' );
4066 if ( $siteurl ) {
4067 define( 'COOKIEHASH', md5( $siteurl ) );
4068 } else {
4069 define( 'COOKIEHASH', '' );
4070 }
4071 }
4072
4073 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4074 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4075 }
4076
4077 /**
4078 * @since 2.5.0
4079 */
4080 if ( ! defined( 'AUTH_COOKIE' ) ) {
4081 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4082 }
4083
4084 /**
4085 * @since 2.6.0
4086 */
4087 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4088 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4089 }
4090 }
4091
4092 /**
4093 * @author Vova Feldman (@svovaf)
4094 * @since 2.1.0
4095 *
4096 * @return int
4097 */
4098 static function get_current_wp_user_id() {
4099 $wp_user = self::_get_current_wp_user();
4100
4101 return $wp_user->ID;
4102 }
4103
4104 /**
4105 * @author Vova Feldman (@svovaf)
4106 * @since 1.2.1.7
4107 *
4108 * @param string $email
4109 *
4110 * @return bool
4111 */
4112 static function is_valid_email( $email ) {
4113 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4114 return false;
4115 }
4116
4117 $parts = explode( '@', $email );
4118
4119 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4120 return false;
4121 }
4122
4123 $blacklist = array(
4124 'admin.',
4125 'webmaster.',
4126 'localhost.',
4127 'dev.',
4128 'development.',
4129 'test.',
4130 'stage.',
4131 'staging.',
4132 );
4133
4134 // Make sure domain is not one of the blacklisted.
4135 foreach ( $blacklist as $invalid ) {
4136 if ( 0 === strpos( $parts[1], $invalid ) ) {
4137 return false;
4138 }
4139 }
4140
4141 // Get the UTF encoded domain name.
4142 $domain = idn_to_ascii( $parts[1] ) . '.';
4143
4144 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4145 }
4146
4147 /**
4148 * Generate API connectivity issue message.
4149 *
4150 * @author Vova Feldman (@svovaf)
4151 * @since 1.0.9
4152 *
4153 * @param mixed $api_result
4154 * @param bool $is_first_failure
4155 */
4156 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
4157 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
4158 // Don't add message if it's the free version and can run anonymously.
4159 return;
4160 }
4161
4162 if ( ! function_exists( 'wp_nonce_url' ) ) {
4163 require_once ABSPATH . 'wp-includes/functions.php';
4164 }
4165
4166 $current_user = self::_get_current_wp_user();
4167 // $admin_email = get_option( 'admin_email' );
4168 $admin_email = $current_user->user_email;
4169
4170 // Aliases.
4171 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
4172 $deactivate_plugin_desc = $this->esc_html_inline( 'We feel your frustration and sincerely apologize for the inconvenience. Hope to see you again in the future.', 'deactivate-plugin-desc' );
4173 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
4174 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
4175 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
4176 $fix_issue_desc = $this->esc_html_inline( 'We will do our best to whitelist your server and resolve this issue ASAP. You will get a follow-up email to %s once we have an update.', 'fix-issue-desc' );
4177 /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
4178 $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
4179 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
4180 $happy_to_resolve_issue_asap = $this->esc_html_inline( 'We are sure it\'s an issue on our side and more than happy to resolve it for you ASAP if you give us a chance.', 'happy-to-resolve-issue-asap' );
4181
4182 $message = false;
4183 if ( is_object( $api_result ) &&
4184 isset( $api_result->error ) &&
4185 isset( $api_result->error->code )
4186 ) {
4187 switch ( $api_result->error->code ) {
4188 case 'curl_missing':
4189 $missing_methods = '';
4190 if ( is_array( $api_result->missing_methods ) &&
4191 ! empty( $api_result->missing_methods )
4192 ) {
4193 foreach ( $api_result->missing_methods as $m ) {
4194 if ( 'curl_version' === $m ) {
4195 continue;
4196 }
4197
4198 if ( ! empty( $missing_methods ) ) {
4199 $missing_methods .= ', ';
4200 }
4201
4202 $missing_methods .= sprintf( '<code>%s</code>', $m );
4203 }
4204
4205 if ( ! empty( $missing_methods ) ) {
4206 $missing_methods = sprintf(
4207 '<br><br><b>%s</b> %s',
4208 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
4209 $missing_methods
4210 );
4211 }
4212 }
4213
4214 $message = sprintf(
4215 $x_requires_access_to_api . ' ' .
4216 $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' ) . ' ' .
4217 $missing_methods .
4218 ' %s',
4219 '<b>' . $this->get_plugin_name() . '</b>',
4220 sprintf(
4221 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4222 sprintf(
4223 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
4224 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
4225 ' - ' . sprintf(
4226 $this->get_text_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'curl-missing-no-clue-desc' ),
4227 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4228 )
4229 ),
4230 sprintf(
4231 '<b>%s</b> - %s',
4232 $sysadmin_title,
4233 esc_html( sprintf( $this->get_text_inline( 'Great, please install cURL and enable it in your php.ini file. In addition, search for the \'disable_functions\' directive in your php.ini file and remove any disabled methods starting with \'curl_\'. To make sure it was successfully activated, use \'phpinfo()\'. Once activated, deactivate the %s and reactivate it back again.', 'curl-missing-sysadmin-desc' ), $this->get_module_label( true ) ) )
4234 ),
4235 sprintf(
4236 '<a href="%s"><b>%s</b></a> - %s',
4237 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4238 $deactivate_plugin_title,
4239 $deactivate_plugin_desc
4240 )
4241 )
4242 );
4243 break;
4244 case 'cloudflare_ddos_protection':
4245 $message = sprintf(
4246 $x_requires_access_to_api . ' ' .
4247 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
4248 $happy_to_resolve_issue_asap .
4249 ' %s',
4250 '<b>' . $this->get_plugin_name() . '</b>',
4251 sprintf(
4252 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4253 sprintf(
4254 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
4255 $fix_issue_title,
4256 ' - ' . sprintf(
4257 $fix_issue_desc,
4258 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4259 )
4260 ),
4261 sprintf(
4262 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
4263 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4264 $install_previous_title,
4265 $install_previous_desc
4266 ),
4267 sprintf(
4268 '<a href="%s"><b>%s</b></a> - %s',
4269 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=' . '', 'deactivate-plugin_' . $this->_plugin_basename ),
4270 $deactivate_plugin_title,
4271 $deactivate_plugin_desc
4272 )
4273 )
4274 );
4275 break;
4276 case 'squid_cache_block':
4277 $message = sprintf(
4278 $x_requires_access_to_api . ' ' .
4279 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
4280 ' %s',
4281 '<b>' . $this->get_plugin_name() . '</b>',
4282 sprintf(
4283 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4284 sprintf(
4285 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
4286 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
4287 sprintf(
4288 $this->esc_html_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'squid-no-clue-desc' ),
4289 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4290 )
4291 ),
4292 sprintf(
4293 '<b>%s</b> - %s',
4294 $sysadmin_title,
4295 sprintf(
4296 $this->esc_html_inline( 'Great, please whitelist the following domains: %s. Once you are done, deactivate the %s and activate it again.', 'squid-sysadmin-desc' ),
4297 // We use a filter since the plugin might require additional API connectivity.
4298 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
4299 'api.freemius.com',
4300 'wp.freemius.com'
4301 ) ) ) . '</b>',
4302 $this->_module_type
4303 )
4304 ),
4305 sprintf(
4306 '<a href="%s"><b>%s</b></a> - %s',
4307 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4308 $deactivate_plugin_title,
4309 $deactivate_plugin_desc
4310 )
4311 )
4312 );
4313 break;
4314 // default:
4315 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
4316 // break;
4317 }
4318 }
4319
4320 $message_id = 'failed_connect_api';
4321 $type = 'error';
4322
4323 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
4324
4325 if ( false === $message ) {
4326 if ( $is_first_failure ) {
4327 // First attempt failed.
4328 $message = sprintf(
4329 $x_requires_access_to_api . ' ' .
4330 $connectivity_test_fails_message . ' ' .
4331 $this->esc_html_inline( 'It\'s probably a temporary issue on our end. Just to be sure, with your permission, would it be o.k to run another connectivity test?', 'connectivity-test-maybe-temporary' ) . '<br><br>' .
4332 '%s',
4333 '<b>' . $this->get_plugin_name() . '</b>',
4334 sprintf(
4335 '<div id="fs_firewall_issue_options">%s %s</div>',
4336 sprintf(
4337 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
4338 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
4339 ),
4340 sprintf(
4341 '<a href="%s" class="button">%s</a>',
4342 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4343 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
4344 )
4345 )
4346 );
4347
4348 $message_id = 'failed_connect_api_first';
4349 $type = 'promotion';
4350 } else {
4351 // Second connectivity attempt failed.
4352 $message = sprintf(
4353 $x_requires_access_to_api . ' ' .
4354 $connectivity_test_fails_message . ' ' .
4355 $happy_to_resolve_issue_asap .
4356 ' %s',
4357 '<b>' . $this->get_plugin_name() . '</b>',
4358 sprintf(
4359 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4360 sprintf(
4361 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
4362 $fix_issue_title,
4363 ' - ' . sprintf(
4364 $fix_issue_desc,
4365 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4366 )
4367 ),
4368 sprintf(
4369 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
4370 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4371 $install_previous_title,
4372 $install_previous_desc
4373 ),
4374 sprintf(
4375 '<a href="%s"><b>%s</b></a> - %s',
4376 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4377 $deactivate_plugin_title,
4378 $deactivate_plugin_desc
4379 )
4380 )
4381 );
4382 }
4383 }
4384
4385 $this->_admin_notices->add_sticky(
4386 $message,
4387 $message_id,
4388 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4389 $type
4390 );
4391 }
4392
4393 /**
4394 * Handle user request to resolve connectivity issue.
4395 * This method will send an email to Freemius API technical staff for resolution.
4396 * The email will contain server's info and installed plugins (might be caching issue).
4397 *
4398 * @author Vova Feldman (@svovaf)
4399 * @since 1.0.9
4400 */
4401 function _email_about_firewall_issue() {
4402 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
4403
4404 $pong = $this->ping();
4405
4406 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4407
4408 if ( $is_connected ) {
4409 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4410
4411 $this->store_connectivity_info( $pong, $is_connected );
4412
4413 echo $this->get_after_plugin_activation_redirect_url();
4414 exit;
4415 }
4416
4417 $current_user = self::_get_current_wp_user();
4418 $admin_email = $current_user->user_email;
4419
4420 $error_type = fs_request_get( 'error_type', 'general' );
4421
4422 switch ( $error_type ) {
4423 case 'squid':
4424 $title = 'Squid ACL Blocking Issue';
4425 break;
4426 case 'cloudflare':
4427 $title = 'CloudFlare Blocking Issue';
4428 break;
4429 default:
4430 $title = 'API Connectivity Issue';
4431 break;
4432 }
4433
4434 $custom_email_sections = array();
4435
4436 // Add 'API Error' custom email section.
4437 $custom_email_sections['api_error'] = array(
4438 'title' => 'API Error',
4439 'rows' => array(
4440 'ping' => array(
4441 'API Error',
4442 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
4443 ),
4444 )
4445 );
4446
4447 // Send email with technical details to resolve API connectivity issues.
4448 $this->send_email(
4449 'api@freemius.com', // recipient
4450 $title . ' [' . $this->get_plugin_name() . ']', // subject
4451 $custom_email_sections,
4452 array( "Reply-To: $admin_email <$admin_email>" ) // headers
4453 );
4454
4455 $this->_admin_notices->add_sticky(
4456 sprintf(
4457 $this->get_text_inline( 'Thank for giving us the chance to fix it! A message was just sent to our technical staff. We will get back to you as soon as we have an update to %s. Appreciate your patience.', 'fix-request-sent-message' ),
4458 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4459 ),
4460 'server_details_sent'
4461 );
4462
4463 // Action was taken, tell that API connectivity troubleshooting should be off now.
4464
4465 echo "1";
4466 exit;
4467 }
4468
4469 /**
4470 * Handle connectivity test retry approved by the user.
4471 *
4472 * @author Vova Feldman (@svovaf)
4473 * @since 1.1.7.4
4474 */
4475 function _retry_connectivity_test() {
4476 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
4477
4478 $pong = $this->ping();
4479
4480 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4481
4482 if ( $is_connected ) {
4483 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4484
4485 $this->store_connectivity_info( $pong, $is_connected );
4486
4487 echo $this->get_after_plugin_activation_redirect_url();
4488 } else {
4489 // Add connectivity issue message after 2nd failed attempt.
4490 $this->_add_connectivity_issue_message( $pong, false );
4491
4492 echo "1";
4493 }
4494
4495 exit;
4496 }
4497
4498 static function _add_firewall_issues_javascript() {
4499 $params = array();
4500 fs_require_once_template( 'firewall-issues-js.php', $params );
4501 }
4502
4503 #endregion
4504
4505 #----------------------------------------------------------------------------------
4506 #region Email
4507 #----------------------------------------------------------------------------------
4508
4509 /**
4510 * Generates and sends an HTML email with customizable sections.
4511 *
4512 * @author Leo Fajardo (@leorw)
4513 * @since 1.1.2
4514 *
4515 * @param string $to_address
4516 * @param string $subject
4517 * @param array $sections
4518 * @param array $headers
4519 *
4520 * @return bool Whether the email contents were sent successfully.
4521 */
4522 private function send_email(
4523 $to_address,
4524 $subject,
4525 $sections = array(),
4526 $headers = array()
4527 ) {
4528 $default_sections = $this->get_email_sections();
4529
4530 // Insert new sections or replace the default email sections.
4531 if ( is_array( $sections ) && ! empty( $sections ) ) {
4532 foreach ( $sections as $section_id => $custom_section ) {
4533 if ( ! isset( $default_sections[ $section_id ] ) ) {
4534 // If the section does not exist, add it.
4535 $default_sections[ $section_id ] = $custom_section;
4536 } else {
4537 // If the section already exists, override it.
4538 $current_section = $default_sections[ $section_id ];
4539
4540 // Replace the current section's title if a custom section title exists.
4541 if ( isset( $custom_section['title'] ) ) {
4542 $current_section['title'] = $custom_section['title'];
4543 }
4544
4545 // Insert new rows under the current section or replace the default rows.
4546 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4547 foreach ( $custom_section['rows'] as $row_id => $row ) {
4548 $current_section['rows'][ $row_id ] = $row;
4549 }
4550 }
4551
4552 $default_sections[ $section_id ] = $current_section;
4553 }
4554 }
4555 }
4556
4557 $vars = array( 'sections' => $default_sections );
4558 $message = fs_get_template( 'email.php', $vars );
4559
4560 // Set the type of email to HTML.
4561 $headers[] = 'Content-type: text/html; charset=UTF-8';
4562
4563 $header_string = implode( "\r\n", $headers );
4564
4565 return wp_mail(
4566 $to_address,
4567 $subject,
4568 $message,
4569 $header_string
4570 );
4571 }
4572
4573 /**
4574 * Generates the data for the sections of the email content.
4575 *
4576 * @author Leo Fajardo (@leorw)
4577 * @since 1.1.2
4578 *
4579 * @return array
4580 */
4581 private function get_email_sections() {
4582 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4583 $current_user = self::_get_current_wp_user();
4584
4585 // Retrieve the cURL version information so that we can get the version number below.
4586 $curl_version_information = curl_version();
4587
4588 $active_plugin = self::get_active_plugins();
4589
4590 // Generate the list of active plugins separated by new line.
4591 $active_plugin_string = '';
4592 foreach ( $active_plugin as $plugin ) {
4593 $active_plugin_string .= sprintf(
4594 '<a href="%s">%s</a> [v%s]<br>',
4595 $plugin['PluginURI'],
4596 $plugin['Name'],
4597 $plugin['Version']
4598 );
4599 }
4600
4601 $server_ip = WP_FS__REMOTE_ADDR;
4602
4603 // Add PHP info for deeper investigation.
4604 ob_start();
4605 phpinfo();
4606 $php_info = ob_get_clean();
4607
4608 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4609
4610 // Generate the default email sections.
4611 $sections = array(
4612 'sdk' => array(
4613 'title' => 'SDK',
4614 'rows' => array(
4615 'fs_version' => array( 'FS Version', $this->version ),
4616 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4617 )
4618 ),
4619 'plugin' => array(
4620 'title' => ucfirst( $this->get_module_type() ),
4621 'rows' => array(
4622 'name' => array( 'Name', $this->get_plugin_name() ),
4623 'version' => array( 'Version', $this->get_plugin_version() )
4624 )
4625 ),
4626 'api' => array(
4627 'title' => 'API Subdomain',
4628 'rows' => array(
4629 'dns' => array(
4630 'DNS_CNAME',
4631 function_exists( 'dns_get_record' ) ?
4632 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4633 'dns_get_record() disabled/blocked'
4634 ),
4635 'ip' => array(
4636 'IP',
4637 function_exists( 'gethostbyname' ) ?
4638 gethostbyname( $api_domain ) :
4639 'gethostbyname() disabled/blocked'
4640 ),
4641 ),
4642 ),
4643 'site' => array(
4644 'title' => 'Site',
4645 'rows' => array(
4646 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4647 'address' => array( 'Address', site_url() ),
4648 'host' => array(
4649 'HTTP_HOST',
4650 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4651 ),
4652 'hosting' => array(
4653 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4654 fs_request_get( 'hosting_company' ) :
4655 'Unknown',
4656 ),
4657 'server_addr' => array(
4658 'SERVER_ADDR',
4659 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4660 )
4661 )
4662 ),
4663 'user' => array(
4664 'title' => 'User',
4665 'rows' => array(
4666 'email' => array( 'Email', $current_user->user_email ),
4667 'first' => array( 'First', $current_user->user_firstname ),
4668 'last' => array( 'Last', $current_user->user_lastname )
4669 )
4670 ),
4671 'plugins' => array(
4672 'title' => 'Plugins',
4673 'rows' => array(
4674 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4675 )
4676 ),
4677 'php_info' => array(
4678 'title' => 'PHP Info',
4679 'rows' => array(
4680 'info' => array( $php_info )
4681 ),
4682 )
4683 );
4684
4685 // Allow the sections to be modified by other code.
4686 $sections = $this->apply_filters( 'email_template_sections', $sections );
4687
4688 return $sections;
4689 }
4690
4691 #endregion
4692
4693 #----------------------------------------------------------------------------------
4694 #region Initialization
4695 #----------------------------------------------------------------------------------
4696
4697 /**
4698 * Init plugin's Freemius instance.
4699 *
4700 * @author Vova Feldman (@svovaf)
4701 * @since 1.0.1
4702 *
4703 * @param number $id
4704 * @param string $public_key
4705 * @param bool $is_live
4706 * @param bool $is_premium
4707 */
4708 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4709 $this->_logger->entrance();
4710
4711 $this->dynamic_init( array(
4712 'id' => $id,
4713 'public_key' => $public_key,
4714 'is_live' => $is_live,
4715 'is_premium' => $is_premium,
4716 ) );
4717 }
4718
4719 /**
4720 * Dynamic initiator, originally created to support initiation
4721 * with parent_id for add-ons.
4722 *
4723 * @author Vova Feldman (@svovaf)
4724 * @since 1.0.6
4725 *
4726 * @param array $plugin_info
4727 *
4728 * @throws Freemius_Exception
4729 */
4730 function dynamic_init( array $plugin_info ) {
4731 $this->_logger->entrance();
4732
4733 $this->parse_settings( $plugin_info );
4734
4735 $this->register_after_settings_parse_hooks();
4736
4737 if ( $this->should_stop_execution() ) {
4738 return;
4739 }
4740
4741 if ( ! $this->is_registered() ) {
4742 if ( $this->is_anonymous() ) {
4743 // If user skipped, no need to test connectivity.
4744 $this->_has_api_connection = true;
4745 $this->_is_on = true;
4746 } else {
4747 if ( ! $this->has_api_connectivity() ) {
4748 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
4749 $this->_admin_notices->has_sticky( 'failed_connect_api' )
4750 ) {
4751 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
4752 // If anonymous mode is disabled, add firewall admin-notice message.
4753 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
4754
4755 $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' );
4756 add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array(
4757 &$this,
4758 '_email_about_firewall_issue'
4759 ) );
4760
4761 add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array(
4762 &$this,
4763 '_retry_connectivity_test'
4764 ) );
4765
4766 /**
4767 * Currently the admin notice manager relies on the module's type and slug. The new AJAX actions manager uses module IDs, hence, consider to replace the if block above with the commented code below after adjusting the admin notices manager to work with module IDs.
4768 *
4769 * @author Vova Feldman (@svovaf)
4770 * @since 2.0.0
4771 */
4772 /*$this->add_ajax_action( 'resolve_firewall_issues', array(
4773 &$this,
4774 '_email_about_firewall_issue'
4775 ) );
4776
4777 $this->add_ajax_action( 'retry_connectivity_test', array(
4778 &$this,
4779 '_retry_connectivity_test'
4780 ) );*/
4781 }
4782 }
4783
4784 return;
4785 } else {
4786 $this->_admin_notices->remove_sticky( array(
4787 'failed_connect_api_first',
4788 'failed_connect_api',
4789 ) );
4790
4791 if ( $this->_anonymous_mode ) {
4792 // Simulate anonymous mode.
4793 $this->_is_anonymous = true;
4794 }
4795 }
4796 }
4797 }
4798
4799 /**
4800 * This should be executed even if Freemius is off for the core module,
4801 * otherwise, the add-ons dialogbox won't work properly. This is esepcially
4802 * relevant when the developer decided to turn FS off for existing users.
4803 *
4804 * @author Vova Feldman (@svovaf)
4805 */
4806 if ( $this->is_user_in_admin() &&
4807 'plugin-information' === fs_request_get( 'tab', false ) &&
4808 $this->should_use_freemius_updater_and_dialog() &&
4809 (
4810 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4811 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4812 )
4813 ) {
4814 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4815
4816 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4817 }
4818
4819 // Check if Freemius is on for the current plugin.
4820 // This MUST be executed after all the plugin variables has been loaded.
4821 if ( ! $this->is_registered() && ! $this->is_on() ) {
4822 return;
4823 }
4824
4825 if ( $this->has_api_connectivity() ) {
4826 if ( self::is_cron() ) {
4827 $this->hook_callback_to_sync_cron();
4828 } else if ( $this->is_user_in_admin() ) {
4829 /**
4830 * Schedule daily data sync cron if:
4831 *
4832 * 1. User opted-in (for tracking).
4833 * 2. If skipped, but later upgraded (opted-in via upgrade).
4834 *
4835 * @author Vova Feldman (@svovaf)
4836 * @since 1.1.7.3
4837 *
4838 */
4839 if ( $this->is_registered() ) {
4840 if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) {
4841 $this->schedule_sync_cron();
4842 }
4843 }
4844
4845 /**
4846 * Check if requested for manual blocking background sync.
4847 */
4848 if ( fs_request_has( 'background_sync' ) ) {
4849 $this->run_manual_sync();
4850 }
4851 }
4852 }
4853
4854 if ( $this->is_registered() ) {
4855 $this->hook_callback_to_install_sync();
4856 }
4857
4858 if ( $this->is_addon() ) {
4859 if ( $this->is_parent_plugin_installed() ) {
4860 // Link to parent FS.
4861 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4862
4863 // Get parent plugin reference.
4864 $this->_parent_plugin = $this->_parent->get_plugin();
4865 }
4866 }
4867
4868 if ( $this->is_user_in_admin() ) {
4869 if ( $this->is_addon() ) {
4870 if ( ! $this->is_parent_plugin_installed() ) {
4871 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4872
4873 if ( isset( $plugin_info['parent'] ) ) {
4874 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4875 }
4876
4877 $this->_admin_notices->add(
4878 ( ! empty( $parent_name ) ?
4879 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 ) :
4880 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() )
4881 ),
4882 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4883 'error'
4884 );
4885
4886 return;
4887 } else {
4888 $is_network_admin = fs_is_network_admin();
4889
4890 if (
4891 $this->_parent->is_registered() &&
4892 ! $this->is_registered() &&
4893 $this->has_free_plan() &&
4894 /**
4895 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4896 * * Network active and in network admin - network activate add-on account.
4897 * * Network active and not in network admin - activate add-on account for the current blog.
4898 * * Not network active and not in network admin - activate add-on account for the current blog.
4899 *
4900 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4901 *
4902 * @author Leo Fajardo (@leorw)
4903 * @since 2.3.0
4904 */
4905 ( $this->is_network_active() || ! $is_network_admin )
4906 ) {
4907 // If parent plugin activated, automatically install add-on for the user.
4908 $this->_activate_addon_account(
4909 $this->_parent,
4910 ( $this->is_network_active() && $is_network_admin ) ?
4911 true :
4912 get_current_blog_id()
4913 );
4914 } else if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4915 // If add-on activated and parent not, automatically install parent for the user.
4916 $this->activate_parent_account( $this->_parent );
4917 }
4918
4919 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4920 if ( $this->is_premium() ) {
4921 // Remove add-on download admin-notice.
4922 $this->_parent->_admin_notices->remove_sticky( array(
4923 'addon_plan_upgraded_' . $this->_slug,
4924 'no_addon_license_' . $this->_slug,
4925 ) );
4926 }
4927
4928 // $this->deactivate_premium_only_addon_without_license();
4929 }
4930 }
4931
4932 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4933
4934 // if ( $this->is_registered() ||
4935 // $this->is_anonymous() ||
4936 // $this->is_pending_activation()
4937 // ) {
4938 // $this->_init_admin();
4939 // }
4940 }
4941
4942 /**
4943 * Should be called outside `$this->is_user_in_admin()` scope
4944 * because the updater has some logic that needs to be executed
4945 * during AJAX calls.
4946 *
4947 * Currently we need to hook to the `http_request_host_is_external` filter.
4948 * In the future, there might be additional logic added.
4949 *
4950 * @author Vova Feldman
4951 * @since 1.2.1.6
4952 */
4953 if (
4954 $this->should_use_freemius_updater_and_dialog() &&
4955 (
4956 $this->is_premium() ||
4957 /**
4958 * If not premium but the premium version is installed, also instantiate the updater so that the
4959 * plugin information dialog of the premium version will have the information from the server.
4960 *
4961 * @author Leo Fajardo (@leorw)
4962 * @since 2.2.3
4963 */
4964 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4965 ) &&
4966 $this->has_release_on_freemius()
4967 ) {
4968 FS_Plugin_Updater::instance( $this );
4969 }
4970
4971 $this->do_action( 'initiated' );
4972
4973 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4974 if ( isset( $this->_storage->prev_is_premium ) ) {
4975 $this->apply_filters(
4976 'after_code_type_change',
4977 // New code type.
4978 $this->_plugin->is_premium
4979 );
4980 } else {
4981 // Set for code type for the first time.
4982 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4983 }
4984 }
4985
4986 if ( ! $this->is_addon() ) {
4987 if ( $this->is_registered() ) {
4988 // Fix for upgrade from versions < 1.0.9.
4989 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4990 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4991 }
4992
4993 $this->do_action( 'after_init_plugin_registered' );
4994 } else if ( $this->is_anonymous() ) {
4995 $this->do_action( 'after_init_plugin_anonymous' );
4996 } else if ( $this->is_pending_activation() ) {
4997 $this->do_action( 'after_init_plugin_pending_activations' );
4998 }
4999 } else {
5000 if ( $this->is_registered() ) {
5001 $this->do_action( 'after_init_addon_registered' );
5002 } else if ( $this->is_anonymous() ) {
5003 $this->do_action( 'after_init_addon_anonymous' );
5004 } else if ( $this->is_pending_activation() ) {
5005 $this->do_action( 'after_init_addon_pending_activations' );
5006 }
5007 }
5008 }
5009
5010 /**
5011 * @author Leo Fajardo (@leorw)
5012 * @since 2.2.3
5013 *
5014 * @return bool
5015 */
5016 private function should_use_freemius_updater_and_dialog() {
5017 return (
5018 /**
5019 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5020 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5021 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5022 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5023 * plugin details from .org).
5024 */
5025 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5026 (
5027 ! self::is_plugin_install_page() &&
5028 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5029 ( 'install-plugin' !== fs_request_get( 'action' ) )
5030 )
5031 );
5032 }
5033
5034 /**
5035 * @author Leo Fajardo (@leorw)
5036 *
5037 * @since 1.2.1.5
5038 */
5039 function _stop_tracking_callback() {
5040 $this->_logger->entrance();
5041
5042 $this->check_ajax_referer( 'stop_tracking' );
5043
5044 $result = $this->stop_tracking( fs_is_network_admin() );
5045
5046 if ( true === $result ) {
5047 self::shoot_ajax_success();
5048 }
5049
5050 $this->_logger->api_error( $result );
5051
5052 self::shoot_ajax_failure(
5053 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
5054 ( $this->is_api_error( $result ) && isset( $result->error ) ?
5055 $result->error->message :
5056 var_export( $result, true ) )
5057 );
5058 }
5059
5060 /**
5061 * @author Leo Fajardo (@leorw)
5062 * @since 1.2.1.5
5063 */
5064 function _allow_tracking_callback() {
5065 $this->_logger->entrance();
5066
5067 $this->check_ajax_referer( 'allow_tracking' );
5068
5069 $result = $this->allow_tracking( fs_is_network_admin() );
5070
5071 if ( true === $result ) {
5072 self::shoot_ajax_success();
5073 }
5074
5075 $this->_logger->api_error( $result );
5076
5077 self::shoot_ajax_failure(
5078 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
5079 ( $this->is_api_error( $result ) && isset( $result->error ) ?
5080 $result->error->message :
5081 var_export( $result, true ) )
5082 );
5083 }
5084
5085 /**
5086 * Opt-out from usage tracking.
5087 *
5088 * Note: This will not delete the account information but will stop all tracking.
5089 *
5090 * Returns:
5091 * 1. FALSE - If the user never opted-in.
5092 * 2. TRUE - If successfully opted-out.
5093 * 3. object - API result on failure.
5094 *
5095 * @author Leo Fajardo (@leorw)
5096 * @since 1.2.1.5
5097 *
5098 * @return bool|object
5099 */
5100 function stop_site_tracking() {
5101 $this->_logger->entrance();
5102
5103 if ( ! $this->is_registered() ) {
5104 // User never opted-in.
5105 return false;
5106 }
5107
5108 if ( $this->is_tracking_prohibited() ) {
5109 // Already disconnected.
5110 return true;
5111 }
5112
5113 // Send update to FS.
5114 $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array(
5115 'is_disconnected' => true
5116 ) );
5117
5118 if ( ! $this->is_api_result_entity( $result ) ||
5119 ! isset( $result->is_disconnected ) ||
5120 ! $result->is_disconnected
5121 ) {
5122 $this->_logger->api_error( $result );
5123
5124 return $result;
5125 }
5126
5127 $this->_site->is_disconnected = $result->is_disconnected;
5128 $this->_store_site();
5129
5130 $this->clear_sync_cron();
5131
5132 // Successfully disconnected.
5133 return true;
5134 }
5135
5136 /**
5137 * Opt-out network from usage tracking.
5138 *
5139 * Note: This will not delete the account information but will stop all tracking.
5140 *
5141 * Returns:
5142 * 1. FALSE - If the user never opted-in.
5143 * 2. TRUE - If successfully opted-out.
5144 * 3. object - API result on failure.
5145 *
5146 * @author Leo Fajardo (@leorw)
5147 * @since 1.2.1.5
5148 *
5149 * @return bool|object
5150 */
5151 function stop_network_tracking() {
5152 $this->_logger->entrance();
5153
5154 if ( ! $this->is_registered() ) {
5155 // User never opted-in.
5156 return false;
5157 }
5158
5159 $install_id_2_blog_id = array();
5160 $installs_map = $this->get_blog_install_map();
5161
5162 $opt_out_all = true;
5163
5164 $params = array();
5165 foreach ( $installs_map as $blog_id => $install ) {
5166 if ( $install->is_tracking_prohibited() ) {
5167 // Already opted-out.
5168 continue;
5169 }
5170
5171 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5172 // Opt-out only from non-delegated installs.
5173 $opt_out_all = false;
5174 continue;
5175 }
5176
5177 $params[] = array( 'id' => $install->id );
5178
5179 $install_id_2_blog_id[ $install->id ] = $blog_id;
5180 }
5181
5182 if ( empty( $install_id_2_blog_id ) ) {
5183 return true;
5184 }
5185
5186 $params[] = array( 'is_disconnected' => true );
5187
5188 // Send update to FS.
5189 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5190
5191 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5192 $this->_logger->api_error( $result );
5193
5194 return $result;
5195 }
5196
5197 foreach ( $result->installs as $r_install ) {
5198 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5199 $install = $installs_map[ $blog_id ];
5200 $install->is_disconnected = $r_install->is_disconnected;
5201 $this->_store_site( true, $blog_id, $install );
5202 }
5203
5204 $this->clear_sync_cron( $opt_out_all );
5205
5206 // Successfully disconnected.
5207 return true;
5208 }
5209
5210 /**
5211 * Opt-out from usage tracking.
5212 *
5213 * Note: This will not delete the account information but will stop all tracking.
5214 *
5215 * Returns:
5216 * 1. FALSE - If the user never opted-in.
5217 * 2. TRUE - If successfully opted-out.
5218 * 3. object - API result on failure.
5219 *
5220 * @author Leo Fajardo (@leorw)
5221 * @since 1.2.1.5
5222 *
5223 * @param bool $is_network_action
5224 *
5225 * @return bool|object
5226 */
5227 function stop_tracking( $is_network_action = false ) {
5228 $this->_logger->entrance();
5229
5230 return $is_network_action ?
5231 $this->stop_network_tracking() :
5232 $this->stop_site_tracking();
5233 }
5234
5235 /**
5236 * Opt-in back into usage tracking.
5237 *
5238 * Note: This will only work if the user opted-in previously.
5239 *
5240 * Returns:
5241 * 1. FALSE - If the user never opted-in.
5242 * 2. TRUE - If successfully opted-in back to usage tracking.
5243 * 3. object - API result on failure.
5244 *
5245 * @author Leo Fajardo (@leorw)
5246 * @since 1.2.1.5
5247 *
5248 * @return bool|object
5249 */
5250 function allow_site_tracking() {
5251 $this->_logger->entrance();
5252
5253 if ( ! $this->is_registered() ) {
5254 // User never opted-in.
5255 return false;
5256 }
5257
5258 if ( $this->is_tracking_allowed() ) {
5259 // Tracking already allowed.
5260 return true;
5261 }
5262
5263 $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array(
5264 'is_disconnected' => false
5265 ) );
5266
5267 if ( ! $this->is_api_result_entity( $result ) ||
5268 ! isset( $result->is_disconnected ) ||
5269 $result->is_disconnected
5270 ) {
5271 $this->_logger->api_error( $result );
5272
5273 return $result;
5274 }
5275
5276 $this->_site->is_disconnected = $result->is_disconnected;
5277 $this->_store_site();
5278
5279 $this->schedule_sync_cron();
5280
5281 // Successfully reconnected.
5282 return true;
5283 }
5284
5285 /**
5286 * Opt-in network back into usage tracking.
5287 *
5288 * Note: This will only work if the user opted-in previously.
5289 *
5290 * Returns:
5291 * 1. FALSE - If the user never opted-in.
5292 * 2. TRUE - If successfully opted-in back to usage tracking.
5293 * 3. object - API result on failure.
5294 *
5295 * @author Leo Fajardo (@leorw)
5296 * @since 1.2.1.5
5297 *
5298 * @return bool|object
5299 */
5300 function allow_network_tracking() {
5301 $this->_logger->entrance();
5302
5303 if ( ! $this->is_registered() ) {
5304 // User never opted-in.
5305 return false;
5306 }
5307
5308 $install_id_2_blog_id = array();
5309 $installs_map = $this->get_blog_install_map();
5310
5311 $params = array();
5312 foreach ( $installs_map as $blog_id => $install ) {
5313 if ( $install->is_tracking_allowed() ) {
5314 continue;
5315 }
5316
5317 $params[] = array( 'id' => $install->id );
5318
5319 $install_id_2_blog_id[ $install->id ] = $blog_id;
5320 }
5321
5322 if ( empty( $install_id_2_blog_id ) ) {
5323 return true;
5324 }
5325
5326 $params[] = array( 'is_disconnected' => false );
5327
5328 // Send update to FS.
5329 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5330
5331
5332 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5333 $this->_logger->api_error( $result );
5334
5335 return $result;
5336 }
5337
5338 foreach ( $result->installs as $r_install ) {
5339 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5340 $install = $installs_map[ $blog_id ];
5341 $install->is_disconnected = $r_install->is_disconnected;
5342 $this->_store_site( true, $blog_id, $install );
5343 }
5344
5345 $this->schedule_sync_cron();
5346
5347 // Successfully reconnected.
5348 return true;
5349 }
5350
5351 /**
5352 * Opt-in back into usage tracking.
5353 *
5354 * Note: This will only work if the user opted-in previously.
5355 *
5356 * Returns:
5357 * 1. FALSE - If the user never opted-in.
5358 * 2. TRUE - If successfully opted-in back to usage tracking.
5359 * 3. object - API result on failure.
5360 *
5361 * @author Leo Fajardo (@leorw)
5362 * @since 1.2.1.5
5363 *
5364 * @param bool $is_network_action
5365 *
5366 * @return bool|object
5367 */
5368 function allow_tracking( $is_network_action = false ) {
5369 $this->_logger->entrance();
5370
5371 return $is_network_action ?
5372 $this->allow_network_tracking() :
5373 $this->allow_site_tracking();
5374 }
5375
5376 /**
5377 * If user opted-in and later disabled usage-tracking,
5378 * re-allow tracking for licensing and updates.
5379 *
5380 * @author Leo Fajardo (@leorw)
5381 * @since 1.2.1.5
5382 *
5383 * @param bool $is_context_single_site
5384 */
5385 private function reconnect_locally( $is_context_single_site = false ) {
5386 $this->_logger->entrance();
5387
5388 if ( ! $this->is_registered() ) {
5389 return;
5390 }
5391
5392 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5393 if ( $this->is_tracking_prohibited() ) {
5394 $this->_site->is_disconnected = false;
5395 $this->_store_site();
5396 }
5397 } else {
5398 $installs_map = $this->get_blog_install_map();
5399 foreach ( $installs_map as $blog_id => $install ) {
5400 /**
5401 * @var FS_Site $install
5402 */
5403 if ( $install->is_tracking_prohibited() ) {
5404 $install->is_disconnected = false;
5405 $this->_store_site( true, $blog_id, $install );
5406 }
5407 }
5408 }
5409 }
5410
5411 /**
5412 * @author Vova Feldman (@svovaf)
5413 * @since 2.3.2
5414 *
5415 * @return bool
5416 */
5417 function is_extensions_tracking_allowed() {
5418 return (true === $this->_storage->get( 'is_extensions_tracking_allowed', true ) );
5419 }
5420
5421 /**
5422 * @author Vova Feldman (@svovaf)
5423 * @since 2.3.2
5424 */
5425 function _update_tracking_permission_callback() {
5426 $this->_logger->entrance();
5427
5428 $this->check_ajax_referer( 'update_tracking_permission' );
5429
5430 $is_enabled = fs_request_get_bool( 'is_enabled', null );
5431
5432 if ( ! is_bool( $is_enabled ) ) {
5433 self::shoot_ajax_failure();
5434 }
5435
5436 $permission = fs_request_get( 'permission' );
5437
5438 switch ( $permission ) {
5439 case 'extensions':
5440 $this->update_extensions_tracking_flag( $is_enabled );
5441 break;
5442 default:
5443 $permission = 'no_match';
5444 }
5445
5446 if ( 'no_match' === $permission ) {
5447 self::shoot_ajax_failure();
5448 }
5449
5450 self::shoot_ajax_success( array(
5451 'permissions' => array(
5452 $permission => $is_enabled,
5453 )
5454 ) );
5455 }
5456
5457 /**
5458 * @author Leo Fajardo (@leorw)
5459 * @since 2.3.2
5460 *
5461 * @param bool $is_enabled
5462 */
5463 private function update_extensions_tracking_flag( $is_enabled ) {
5464 $this->_storage->store( 'is_extensions_tracking_allowed', $is_enabled );
5465 }
5466
5467 /**
5468 * Parse plugin's settings (as defined by the plugin dev).
5469 *
5470 * @author Vova Feldman (@svovaf)
5471 * @since 1.1.7.3
5472 *
5473 * @param array $plugin_info
5474 *
5475 * @throws \Freemius_Exception
5476 */
5477 private function parse_settings( &$plugin_info ) {
5478 $this->_logger->entrance();
5479
5480 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5481 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5482 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5483 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5484 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5485
5486 /**
5487 * @author Vova Feldman (@svovaf)
5488 * @since 1.1.9 Try to pull secret key from external config.
5489 */
5490 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5491 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5492 }
5493
5494 if ( isset( $plugin_info['parent'] ) ) {
5495 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5496 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5497 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5498 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5499 }
5500
5501 if ( false === $id ) {
5502 throw new Freemius_Exception( array(
5503 'error' => array(
5504 'type' => 'ParameterNotSet',
5505 'message' => 'Plugin id parameter is not set.',
5506 'code' => 'plugin_id_not_set',
5507 'http' => 500,
5508 )
5509 ) );
5510 }
5511 if ( false === $public_key ) {
5512 throw new Freemius_Exception( array(
5513 'error' => array(
5514 'type' => 'ParameterNotSet',
5515 'message' => 'Plugin public_key parameter is not set.',
5516 'code' => 'plugin_public_key_not_set',
5517 'http' => 500,
5518 )
5519 ) );
5520 }
5521
5522 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5523 $this->_plugin :
5524 new FS_Plugin();
5525
5526 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5527
5528 $plugin->update( array(
5529 'id' => $id,
5530 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5531 'public_key' => $public_key,
5532 'slug' => $this->_slug,
5533 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5534 'parent_plugin_id' => $parent_id,
5535 'version' => $this->get_plugin_version(),
5536 'title' => $this->get_plugin_name( $premium_suffix ),
5537 'file' => $this->_plugin_basename,
5538 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5539 'premium_suffix' => $premium_suffix,
5540 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5541 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5542 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5543 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5544 ) );
5545
5546 if ( $plugin->is_updated() ) {
5547 // Update plugin details.
5548 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5549 }
5550 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5551 $this->_plugin->secret_key = $secret_key;
5552
5553 /**
5554 * 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).
5555 *
5556 * @author Vova Feldman
5557 * @since 2.4.5
5558 */
5559 if ( $this->is_network_active() && fs_is_network_admin() ) {
5560 if ( isset( $plugin_info['menu_network'] ) &&
5561 is_array( $plugin_info['menu_network'] ) &&
5562 ! empty( $plugin_info['menu_network'] )
5563 ) {
5564 $plugin_info['menu'] = $plugin_info['menu_network'];
5565 }
5566 }
5567
5568 if ( ! isset( $plugin_info['menu'] ) ) {
5569 $plugin_info['menu'] = array();
5570
5571 if ( ! empty( $this->_storage->sdk_last_version ) &&
5572 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5573 ) {
5574 // Backward compatibility to 1.1.2
5575 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5576 $plugin_info['menu_slug'] :
5577 $this->_slug;
5578 }
5579 }
5580
5581 $this->_menu = FS_Admin_Menu_Manager::instance(
5582 $this->_module_id,
5583 $this->_module_type,
5584 $this->get_unique_affix()
5585 );
5586
5587 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5588
5589 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5590 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5591 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5592 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5593 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5594 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5595 if ( $this->_is_premium_only ) {
5596 // If premium only plugin, disable anonymous mode.
5597 $this->_enable_anonymous = false;
5598 $this->_anonymous_mode = false;
5599 } else {
5600 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5601 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5602 }
5603 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5604
5605 if ( ! empty( $plugin_info['trial'] ) ) {
5606 $this->_trial_days = $this->get_numeric_option(
5607 $plugin_info['trial'],
5608 'days',
5609 // Default to 0 - trial without days specification.
5610 0
5611 );
5612
5613 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5614 }
5615
5616 $this->_navigation = $this->get_option(
5617 $plugin_info,
5618 'navigation',
5619 $this->is_free_wp_org_theme() ?
5620 self::NAVIGATION_TABS :
5621 self::NAVIGATION_MENU
5622 );
5623 }
5624
5625 /**
5626 * @param string[] $options
5627 * @param string $key
5628 * @param mixed $default
5629 *
5630 * @return bool
5631 */
5632 private function get_option( &$options, $key, $default = false ) {
5633 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5634 }
5635
5636 private function get_bool_option( &$options, $key, $default = false ) {
5637 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5638 }
5639
5640 private function get_numeric_option( &$options, $key, $default = false ) {
5641 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5642 }
5643
5644 /**
5645 * Gate keeper.
5646 *
5647 * @author Vova Feldman (@svovaf)
5648 * @since 1.1.7.3
5649 *
5650 * @return bool
5651 */
5652 private function should_stop_execution() {
5653 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5654 /**
5655 * Don't execute Freemius until plugin was fully loaded at least once,
5656 * to give the opportunity for the activation hook to run before pinging
5657 * the API for connectivity test. This logic is relevant for the
5658 * identification of new plugin install vs. plugin update.
5659 *
5660 * @author Vova Feldman (@svovaf)
5661 * @since 1.1.9
5662 */
5663 return true;
5664 }
5665
5666 if ( $this->is_activation_mode() ) {
5667 if ( ! is_admin() ) {
5668 /**
5669 * If in activation mode, don't execute Freemius outside of the
5670 * admin dashboard.
5671 *
5672 * @author Vova Feldman (@svovaf)
5673 * @since 1.1.7.3
5674 */
5675 return true;
5676 }
5677
5678 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5679 /**
5680 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5681 * then don't start Freemius.
5682 *
5683 * @author Vova Feldman (@svovaf)
5684 * @since 1.1.6.3
5685 *
5686 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5687 */
5688 return true;
5689 }
5690
5691 if ( self::is_cron() ) {
5692 /**
5693 * If in activation mode, don't execute Freemius during wp crons
5694 * (wp crons have HTTP context - called as HTTP request).
5695 *
5696 * @author Vova Feldman (@svovaf)
5697 * @since 1.1.7.3
5698 */
5699 return true;
5700 }
5701
5702 if ( self::is_ajax() &&
5703 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
5704 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
5705 ) {
5706 /**
5707 * During activation, if running in AJAX mode, unless there's a sticky
5708 * connectivity issue notice, don't run Freemius.
5709 *
5710 * @author Vova Feldman (@svovaf)
5711 * @since 1.1.7.3
5712 */
5713 return true;
5714 }
5715 }
5716
5717 return false;
5718 }
5719
5720 /**
5721 * Triggered after code type has changed.
5722 *
5723 * @author Vova Feldman (@svovaf)
5724 * @since 1.1.9.1
5725 */
5726 function _after_code_type_change() {
5727 $this->_logger->entrance();
5728
5729 if ( $this->is_theme() ) {
5730 // Expire the cache of the previous tabs since the theme may
5731 // have setting updates after code type has changed.
5732 $this->_cache->expire( 'tabs' );
5733 $this->_cache->expire( 'tabs_stylesheets' );
5734 }
5735
5736 if ( $this->is_registered() ) {
5737 if ( ! $this->is_addon() ) {
5738 add_action(
5739 is_admin() ? 'admin_init' : 'init',
5740 array( &$this, '_plugin_code_type_changed' )
5741 );
5742 }
5743
5744 if ( $this->is_premium() ) {
5745 // Purge cached payments after switching to the premium version.
5746 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5747 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5748 }
5749 }
5750 }
5751
5752 /**
5753 * Handles plugin's code type change (free <--> premium).
5754 *
5755 * @author Vova Feldman (@svovaf)
5756 * @since 1.0.9
5757 */
5758 function _plugin_code_type_changed() {
5759 $this->_logger->entrance();
5760
5761 if ( $this->is_premium() ) {
5762 $this->reconnect_locally();
5763
5764 // Activated premium code.
5765 $this->do_action( 'after_premium_version_activation' );
5766
5767 // Remove all sticky messages related to download of the premium version.
5768 $this->_admin_notices->remove_sticky( array(
5769 'trial_started',
5770 'plan_upgraded',
5771 'plan_changed',
5772 'license_activated',
5773 ) );
5774
5775 $notice = '';
5776 if ( ! $this->is_only_premium() ) {
5777 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5778 }
5779
5780 $license_notice = $this->get_license_network_activation_notice();
5781 if ( ! empty( $license_notice ) ) {
5782 $notice .= ' ' . $license_notice;
5783 }
5784
5785 if ( ! empty( $notice ) ) {
5786 $this->_admin_notices->add_sticky(
5787 trim( $notice ),
5788 'premium_activated',
5789 $this->get_text_x_inline( 'W00t',
5790 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5791 );
5792 }
5793 } else {
5794 // Remove sticky message related to premium code activation.
5795 $this->_admin_notices->remove_sticky( 'premium_activated' );
5796
5797 // Activated free code (after had the premium before).
5798 $this->do_action( 'after_free_version_reactivation' );
5799
5800 if ( $this->is_paying() && ! $this->is_premium() ) {
5801 $this->_admin_notices->add_sticky(
5802 sprintf(
5803 /* translators: %s: License type (e.g. you have a professional license) */
5804 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5805 $this->get_plan_title()
5806 ) . $this->get_complete_upgrade_instructions(),
5807 'plan_upgraded',
5808 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
5809 );
5810 }
5811 }
5812
5813 // Schedule code type changes event.
5814 $this->schedule_install_sync();
5815
5816 /**
5817 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5818 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5819 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5820 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5821 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5822 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5823 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5824 */
5825 $this->unregister_uninstall_hook();
5826
5827 $this->clear_module_main_file_cache();
5828
5829 // Update is_premium of latest version.
5830 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5831 }
5832
5833 #endregion
5834
5835 #----------------------------------------------------------------------------------
5836 #region Add-ons
5837 #----------------------------------------------------------------------------------
5838
5839 /**
5840 * Check if add-on installed and activated on site.
5841 *
5842 * @author Vova Feldman (@svovaf)
5843 * @since 1.0.6
5844 *
5845 * @param string|number $id_or_slug
5846 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5847 *
5848 * @return bool
5849 */
5850 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5851 $this->_logger->entrance();
5852
5853 $addon_id = self::get_module_id( $id_or_slug );
5854 $is_activated = self::has_instance( $addon_id );
5855
5856 if ( ! $is_activated ) {
5857 return false;
5858 }
5859
5860 if ( is_bool( $is_premium ) ) {
5861 // Check if the specified code version is activate.
5862 $addon = $this->get_addon_instance( $addon_id );
5863 $is_activated = ( $is_premium === $addon->is_premium() );
5864 }
5865
5866 return $is_activated;
5867 }
5868
5869 /**
5870 * Check if add-on was connected to install
5871 *
5872 * @author Vova Feldman (@svovaf)
5873 * @since 1.1.7
5874 *
5875 * @param string|number $id_or_slug
5876 *
5877 * @return bool
5878 */
5879 function is_addon_connected( $id_or_slug ) {
5880 $this->_logger->entrance();
5881
5882 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5883
5884 $addon_id = self::get_module_id( $id_or_slug );
5885 $addon = $this->get_addon( $addon_id );
5886 $slug = $addon->slug;
5887 if ( ! isset( $sites[ $slug ] ) ) {
5888 return false;
5889 }
5890
5891 $site = $sites[ $slug ];
5892
5893 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5894
5895 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5896 // The given slug do NOT belong to any of the plugin's add-ons.
5897 return false;
5898 }
5899
5900 return ( is_object( $site ) &&
5901 is_numeric( $site->id ) &&
5902 is_numeric( $site->user_id ) &&
5903 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5904 );
5905 }
5906
5907 /**
5908 * Determines if add-on installed.
5909 *
5910 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5911 *
5912 * @author Vova Feldman (@svovaf)
5913 * @since 1.0.6
5914 *
5915 * @param string|number $id_or_slug
5916 *
5917 * @return bool
5918 */
5919 function is_addon_installed( $id_or_slug ) {
5920 $this->_logger->entrance();
5921
5922 $addon_id = self::get_module_id( $id_or_slug );
5923
5924 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5925 }
5926
5927 /**
5928 * Get add-on basename.
5929 *
5930 * @author Vova Feldman (@svovaf)
5931 * @since 1.0.6
5932 *
5933 * @param string|number $id_or_slug
5934 *
5935 * @return string
5936 */
5937 function get_addon_basename( $id_or_slug ) {
5938 $addon_id = self::get_module_id( $id_or_slug );
5939
5940 if ( $this->is_addon_activated( $addon_id ) ) {
5941 return self::instance( $addon_id )->get_plugin_basename();
5942 }
5943
5944 $addon = $this->get_addon( $addon_id );
5945 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5946
5947 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5948 return $premium_basename;
5949 }
5950
5951 $all_plugins = $this->get_all_plugins();
5952
5953 foreach ( $all_plugins as $basename => $data ) {
5954 if ( $addon->slug === $data['slug'] ||
5955 $addon->premium_slug === $data['slug']
5956 ) {
5957 return $basename;
5958 }
5959 }
5960
5961 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5962
5963 return $free_basename;
5964 }
5965
5966 /**
5967 * Get installed add-ons instances.
5968 *
5969 * @author Vova Feldman (@svovaf)
5970 * @since 1.0.6
5971 *
5972 * @return Freemius[]
5973 */
5974 function get_installed_addons() {
5975 if ( $this->is_addon() ) {
5976 // Add-on cannot have add-ons.
5977 return array();
5978 }
5979
5980 $installed_addons = array();
5981
5982 foreach ( self::$_instances as $instance ) {
5983 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5984 $installed_addons[] = $instance;
5985 }
5986 }
5987
5988 return $installed_addons;
5989 }
5990
5991 /**
5992 * Check if any add-ons of the plugin are installed.
5993 *
5994 * @author Leo Fajardo (@leorw)
5995 * @since 1.1.1
5996 *
5997 * @return bool
5998 */
5999 function has_installed_addons() {
6000 if ( ! $this->has_addons() ) {
6001 return false;
6002 }
6003
6004 foreach ( self::$_instances as $instance ) {
6005 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
6006 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
6007 return true;
6008 }
6009 }
6010 }
6011
6012 return false;
6013 }
6014
6015 /**
6016 * Tell Freemius that the current plugin is an add-on.
6017 *
6018 * @author Vova Feldman (@svovaf)
6019 * @since 1.0.6
6020 *
6021 * @param number $parent_plugin_id The parent plugin ID
6022 */
6023 function init_addon( $parent_plugin_id ) {
6024 $this->_plugin->parent_plugin_id = $parent_plugin_id;
6025 }
6026
6027 /**
6028 * @author Vova Feldman (@svovaf)
6029 * @since 1.0.6
6030 *
6031 * @return bool
6032 */
6033 function is_addon() {
6034 return (
6035 isset( $this->_plugin->parent_plugin_id ) &&
6036 is_numeric( $this->_plugin->parent_plugin_id )
6037 );
6038 }
6039
6040 /**
6041 * @author Vova Feldman (@svovaf)
6042 * @since 2.3.2
6043 *
6044 * @param number $parent_product_id
6045 *
6046 * @return bool
6047 */
6048 function is_addon_of( $parent_product_id ) {
6049 return (
6050 $this->is_addon() &&
6051 $parent_product_id == $this->_plugin->parent_plugin_id
6052 );
6053 }
6054
6055 /**
6056 * Deactivate add-on if it's premium only and the user does't have a valid license.
6057 *
6058 * @param bool $is_after_trial_cancel
6059 *
6060 * @return bool If add-on was deactivated.
6061 */
6062 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6063 if ( ! $this->has_free_plan() &&
6064 ! $this->has_features_enabled_license() &&
6065 ! $this->_has_premium_license()
6066 ) {
6067 if ( $this->is_registered() ) {
6068 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6069 // if (empty($this->_storage->activation_timestamp) ||
6070 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6071 // ) {
6072 /**
6073 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6074 *
6075 * Retry syncing the user add-on licenses.
6076 */
6077 // Sync licenses.
6078 $this->_sync_licenses();
6079 // }
6080
6081 // Try to activate premium license.
6082 $this->_activate_license( true );
6083 }
6084
6085 if ( ! $this->has_free_plan() &&
6086 ! $this->has_features_enabled_license() &&
6087 ! $this->_has_premium_license()
6088 ) {
6089 // @todo Check if deactivate plugins also call the deactivation hook.
6090
6091 $this->_parent->_admin_notices->add_sticky(
6092 sprintf(
6093 ( $is_after_trial_cancel ?
6094 $this->_parent->get_text_inline(
6095 '%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.',
6096 'addon-trial-cancelled-message'
6097 ) :
6098 $this->_parent->get_text_inline(
6099 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6100 'addon-no-license-message'
6101 )
6102 ),
6103 '<b>' . $this->_plugin->title . '</b>'
6104 ) . ' ' . sprintf(
6105 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6106 $this->_parent->addon_url( $this->_slug ),
6107 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6108 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6109 ),
6110 'no_addon_license_' . $this->_slug,
6111 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6112 ( $is_after_trial_cancel ? 'success' : 'error' )
6113 );
6114
6115 deactivate_plugins( array( $this->_plugin_basename ), true );
6116
6117 return true;
6118 }
6119 }
6120
6121 return false;
6122 }
6123
6124 #endregion
6125
6126 #----------------------------------------------------------------------------------
6127 #region Sandbox
6128 #----------------------------------------------------------------------------------
6129
6130 /**
6131 * Set Freemius into sandbox mode for debugging.
6132 *
6133 * @author Vova Feldman (@svovaf)
6134 * @since 1.0.4
6135 *
6136 * @param string $secret_key
6137 */
6138 function init_sandbox( $secret_key ) {
6139 $this->_plugin->secret_key = $secret_key;
6140
6141 // Update plugin details.
6142 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6143 }
6144
6145 /**
6146 * Check if running payments in sandbox mode.
6147 *
6148 * @author Vova Feldman (@svovaf)
6149 * @since 1.0.4
6150 *
6151 * @return bool
6152 */
6153 function is_payments_sandbox() {
6154 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6155 }
6156
6157 #endregion
6158
6159 /**
6160 * Check if running test vs. live plugin.
6161 *
6162 * @author Vova Feldman (@svovaf)
6163 * @since 1.0.5
6164 *
6165 * @return bool
6166 */
6167 function is_live() {
6168 return $this->_plugin->is_live;
6169 }
6170
6171 /**
6172 * Check if super-admin skipped connection for all sites in the network.
6173 *
6174 * @author Vova Feldman (@svovaf)
6175 * @since 2.0.0
6176 */
6177 function is_network_anonymous() {
6178 if ( ! $this->_is_network_active ) {
6179 return false;
6180 }
6181
6182 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6183
6184 if ( empty( $is_anonymous_ms ) ) {
6185 return false;
6186 }
6187
6188 return $is_anonymous_ms['is'];
6189 }
6190
6191 /**
6192 * Check if super-admin opted-in for all sites in the network.
6193 *
6194 * @author Vova Feldman (@svovaf)
6195 * @since 2.0.0
6196 */
6197 function is_network_connected() {
6198 if ( ! $this->_is_network_active ) {
6199 return false;
6200 }
6201
6202 return $this->_storage->get( 'is_network_connected' );
6203 }
6204
6205 /**
6206 * Check if the user skipped connecting the account with Freemius.
6207 *
6208 * @author Vova Feldman (@svovaf)
6209 * @since 1.0.7
6210 *
6211 * @return bool
6212 */
6213 function is_anonymous() {
6214 if ( ! isset( $this->_is_anonymous ) ) {
6215 if ( $this->is_network_anonymous() ) {
6216 $this->_is_anonymous = true;
6217 } else if ( ! fs_is_network_admin() ) {
6218 if ( ! isset( $this->_storage->is_anonymous ) ) {
6219 // Not skipped.
6220 $this->_is_anonymous = false;
6221 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6222 // For back compatibility, since the variable was boolean before.
6223 $this->_is_anonymous = $this->_storage->is_anonymous;
6224
6225 // Upgrade stored data format to 1.1.3 format.
6226 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6227 } else {
6228 // Version 1.1.3 and later.
6229 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6230 }
6231 }
6232 }
6233
6234 return $this->_is_anonymous;
6235 }
6236
6237 /**
6238 * Check if the user skipped the connection of a specified site.
6239 *
6240 * @author Vova Feldman (@svovaf)
6241 * @since 2.0.0
6242 *
6243 * @param int $blog_id
6244 *
6245 * @return bool
6246 */
6247 function is_anonymous_site( $blog_id = 0 ) {
6248 if ( $this->is_network_anonymous() ) {
6249 return true;
6250 }
6251
6252 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6253
6254 if ( empty( $is_anonymous ) ) {
6255 return false;
6256 }
6257
6258 return $is_anonymous['is'];
6259 }
6260
6261 /**
6262 * Check if user connected his account and install pending email activation.
6263 *
6264 * @author Vova Feldman (@svovaf)
6265 * @since 1.0.7
6266 *
6267 * @return bool
6268 */
6269 function is_pending_activation() {
6270 return $this->_storage->get( 'is_pending_activation', false );
6271 }
6272
6273 /**
6274 * Check if plugin must be WordPress.org compliant.
6275 *
6276 * @since 1.0.7
6277 *
6278 * @return bool
6279 */
6280 function is_org_repo_compliant() {
6281 return $this->_is_org_compliant;
6282 }
6283
6284 #--------------------------------------------------------------------------------
6285 #region WP Cron Common
6286 #--------------------------------------------------------------------------------
6287
6288 /**
6289 * @author Vova Feldman (@svovaf)
6290 * @since 2.0.0
6291 *
6292 * @param string $name Cron name.
6293 *
6294 * @return object
6295 */
6296 private function get_cron_data( $name ) {
6297 $this->_logger->entrance( $name );
6298
6299 /**
6300 * @var object $cron_data
6301 */
6302 return $this->_storage->get( "{$name}_cron", null );
6303 }
6304
6305 /**
6306 * @author Vova Feldman (@svovaf)
6307 * @since 2.0.0
6308 *
6309 * @param string $name Cron name.
6310 */
6311 private function clear_cron_data( $name ) {
6312 $this->_logger->entrance( $name );
6313
6314 $this->_storage->remove( "{$name}_cron" );
6315 }
6316
6317 /**
6318 * @author Vova Feldman (@svovaf)
6319 * @since 2.0.0
6320 *
6321 * @param string $name Cron name.
6322 * @param int $cron_blog_id The cron executing blog ID.
6323 */
6324 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6325 $this->_logger->entrance( $name );
6326
6327 $this->_storage->store( "{$name}_cron", (object) array(
6328 'version' => $this->get_plugin_version(),
6329 'blog_id' => $cron_blog_id,
6330 'sdk_version' => $this->version,
6331 'timestamp' => WP_FS__SCRIPT_START_TIME,
6332 'on' => true,
6333 ) );
6334 }
6335
6336 /**
6337 * Get the cron's executing blog ID.
6338 *
6339 * @author Vova Feldman (@svovaf)
6340 * @since 2.0.0
6341 *
6342 * @param string $name Cron name.
6343 *
6344 * @return int
6345 */
6346 private function get_cron_blog_id( $name ) {
6347 $this->_logger->entrance( $name );
6348
6349 /**
6350 * @var object $cron_data
6351 */
6352 $cron_data = $this->get_cron_data( $name );
6353
6354 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6355 $cron_data->blog_id :
6356 0;
6357 }
6358
6359 /**
6360 * @author Vova Feldman (@svovaf)
6361 * @since 2.0.0
6362 *
6363 * @param string $name Cron name.
6364 *
6365 * @return bool
6366 */
6367 private function is_cron_on( $name ) {
6368 $this->_logger->entrance( $name );
6369
6370 /**
6371 * @var object $cron_data
6372 */
6373 $cron_data = $this->get_cron_data( $name );
6374
6375 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6376 }
6377
6378 /**
6379 * Unix timestamp for previous cron execution or false if never executed.
6380 *
6381 * @author Vova Feldman (@svovaf)
6382 * @since 2.0.0
6383 *
6384 * @param string $name Cron name.
6385 *
6386 * @return int|false
6387 */
6388 private function cron_last_execution( $name ) {
6389 $this->_logger->entrance( $name );
6390
6391 return $this->_storage->get( "{$name}_timestamp" );
6392 }
6393
6394 /**
6395 * Set cron execution time to now.
6396 *
6397 * @author Vova Feldman (@svovaf)
6398 * @since 2.0.0
6399 *
6400 * @param string $name Cron name.
6401 */
6402 private function set_cron_execution_timestamp( $name ) {
6403 $this->_logger->entrance( $name );
6404
6405 $this->_storage->store( "{$name}_timestamp", time() );
6406 }
6407
6408 /**
6409 * Sets the keepalive time to now.
6410 *
6411 * @author Leo Fajardo (@leorw)
6412 * @since 2.2.3
6413 *
6414 * @param bool|null $use_network_level_storage
6415 */
6416 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6417 $this->_logger->entrance();
6418
6419 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6420 }
6421
6422 /**
6423 * Check if cron was executed in the last $period of seconds.
6424 *
6425 * @author Vova Feldman (@svovaf)
6426 * @since 2.0.0
6427 *
6428 * @param string $name Cron name.
6429 * @param int $period In seconds
6430 *
6431 * @return bool
6432 */
6433 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6434 $this->_logger->entrance( $name );
6435
6436 $last_execution = $this->cron_last_execution( $name );
6437
6438 if ( ! is_numeric( $last_execution ) ) {
6439 return false;
6440 }
6441
6442 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6443 }
6444
6445 /**
6446 * WP Cron is executed on a site level. When running in a multisite network environment
6447 * with the network integration activated, for optimization reasons, we are consolidating
6448 * the installs data sync cron to be executed only from a single site.
6449 *
6450 * @author Vova Feldman (@svovaf)
6451 * @since 2.0.0
6452 *
6453 * @param int $except_blog_id Target any except the excluded blog ID.
6454 *
6455 * @return int
6456 */
6457 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6458 if ( ! is_multisite() ) {
6459 return 0;
6460 }
6461
6462 if ( $this->_is_network_active &&
6463 is_numeric( $this->_storage->network_install_blog_id ) &&
6464 $except_blog_id != $this->_storage->network_install_blog_id &&
6465 self::is_site_active( $this->_storage->network_install_blog_id )
6466 ) {
6467 // Try to run cron from the main network blog.
6468 $install = $this->get_install_by_blog_id( $this->_storage->network_install_blog_id );
6469
6470 if ( is_object( $install ) &&
6471 ( $this->is_premium() || $install->is_tracking_allowed() )
6472 ) {
6473 return $this->_storage->network_install_blog_id;
6474 }
6475 }
6476
6477 // Get first opted-in blog ID with active tracking.
6478 $installs = $this->get_blog_install_map();
6479 foreach ( $installs as $blog_id => $install ) {
6480 if ( $except_blog_id != $blog_id &&
6481 self::is_site_active( $blog_id ) &&
6482 ( $this->is_premium() || $install->is_tracking_allowed() )
6483 ) {
6484 return $blog_id;
6485 }
6486 }
6487
6488 return 0;
6489 }
6490
6491 /**
6492 * @author Vova Feldman (@svovaf)
6493 * @since 2.0.0
6494 *
6495 * @param string $name Cron name.
6496 * @param string $action_tag Callback action tag.
6497 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6498 */
6499 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6500 $this->_logger->entrance( $name );
6501
6502 if ( ! $this->is_cron_on( $name ) ) {
6503 return;
6504 }
6505
6506 $clear_cron = true;
6507 if ( ! $is_network_clear && $this->_is_network_active ) {
6508 $installs = $this->get_blog_install_map();
6509
6510 foreach ( $installs as $blog_id => $install ) {
6511 /**
6512 * @var FS_Site $install
6513 */
6514 if ( $install->is_tracking_allowed() ) {
6515 $clear_cron = false;
6516 break;
6517 }
6518 }
6519 }
6520
6521 if ( ! $clear_cron ) {
6522 return;
6523 }
6524
6525 /**
6526 * @var object $cron_data
6527 */
6528 $cron_data = $this->get_cron_data( $name );
6529
6530 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6531 $cron_data->blog_id :
6532 0;
6533
6534 $this->clear_cron_data( $name );
6535
6536 if ( 0 < $cron_blog_id ) {
6537 switch_to_blog( $cron_blog_id );
6538 }
6539
6540 if ( empty( $action_tag ) ) {
6541 $action_tag = $name;
6542 }
6543
6544 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6545
6546 if ( 0 < $cron_blog_id ) {
6547 restore_current_blog();
6548 }
6549 }
6550
6551 /**
6552 * Unix timestamp for next cron execution or false if not scheduled.
6553 *
6554 * @author Vova Feldman (@svovaf)
6555 * @since 2.0.0
6556 *
6557 * @param string $name Cron name.
6558 * @param string $action_tag Callback action tag.
6559 *
6560 * @return int|false
6561 */
6562 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6563 $this->_logger->entrance( $name );
6564
6565 if ( ! $this->is_cron_on( $name ) ) {
6566 return false;
6567 }
6568
6569 /**
6570 * @var object $cron_data
6571 */
6572 $cron_data = $this->get_cron_data( $name );
6573
6574 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6575 $cron_data->blog_id :
6576 0;
6577
6578 if ( 0 < $cron_blog_id ) {
6579 switch_to_blog( $cron_blog_id );
6580 }
6581
6582 if ( empty( $action_tag ) ) {
6583 $action_tag = $name;
6584 }
6585
6586 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6587
6588 if ( 0 < $cron_blog_id ) {
6589 restore_current_blog();
6590 }
6591
6592 return $next_scheduled;
6593 }
6594
6595 /**
6596 * @author Vova Feldman (@svovaf)
6597 * @since 2.0.0
6598 *
6599 * @param string $name Cron name.
6600 * @param string $action_tag Callback action tag.
6601 * @param string $recurrence 'single' or 'daily'.
6602 * @param int $start_at Defaults to now.
6603 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6604 * @param int $except_blog_id Target any except the excluded blog ID.
6605 */
6606 private function schedule_cron(
6607 $name,
6608 $action_tag = '',
6609 $recurrence = 'single',
6610 $start_at = WP_FS__SCRIPT_START_TIME,
6611 $randomize_start = true,
6612 $except_blog_id = 0
6613 ) {
6614 $this->_logger->entrance( $name );
6615
6616 $this->clear_cron( $name, $action_tag, true );
6617
6618 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6619
6620 if ( is_multisite() && 0 == $cron_blog_id ) {
6621 // Don't schedule cron since couldn't find a target blog.
6622 return;
6623 }
6624
6625 if ( 0 < $cron_blog_id ) {
6626 switch_to_blog( $cron_blog_id );
6627 }
6628
6629 if ( 'daily' === $recurrence ) {
6630 if ( $randomize_start ) {
6631 // Schedule first sync with a random 12 hour time range from now.
6632 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6633 }
6634
6635 // Schedule daily WP cron.
6636 wp_schedule_event(
6637 $start_at,
6638 'daily',
6639 $this->get_action_tag( $action_tag )
6640 );
6641 } else if ( 'single' === $recurrence ) {
6642 // Schedule single cron.
6643 wp_schedule_single_event(
6644 $start_at,
6645 $this->get_action_tag( $action_tag )
6646 );
6647 }
6648
6649 $this->set_cron_data( $name, $cron_blog_id );
6650
6651 if ( 0 < $cron_blog_id ) {
6652 restore_current_blog();
6653 }
6654 }
6655
6656 /**
6657 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6658 * that doesn't halt page loading.
6659 *
6660 * @author Vova Feldman (@svovaf)
6661 * @since 2.0.0
6662 *
6663 * @param string $name Cron name.
6664 * @param callable $callable The function that should be executed.
6665 */
6666 private function execute_cron( $name, $callable ) {
6667 $this->_logger->entrance( $name );
6668
6669 // Store the last time data sync was executed.
6670 $this->set_cron_execution_timestamp( $name );
6671
6672 // Check if API is temporary down.
6673 if ( FS_Api::is_temporary_down() ) {
6674 return;
6675 }
6676
6677 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6678
6679 $users_2_blog_ids = array();
6680
6681 if ( ! is_multisite() ) {
6682 // Add dummy blog.
6683 $users_2_blog_ids[0] = array( 0 );
6684 } else {
6685 $installs = $this->get_blog_install_map();
6686 foreach ( $installs as $blog_id => $install ) {
6687 if ( $this->is_premium() || $install->is_tracking_allowed() ) {
6688 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6689 $users_2_blog_ids[ $install->user_id ] = array();
6690 }
6691
6692 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6693 }
6694 }
6695 }
6696
6697 $current_blog_id = get_current_blog_id();
6698
6699 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6700 if ( 0 < $blog_ids[0] ) {
6701 $this->switch_to_blog( $blog_ids[0] );
6702 }
6703
6704 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6705
6706 foreach ( $blog_ids as $blog_id ) {
6707 $this->do_action( "after_{$name}_cron", $blog_id );
6708 }
6709 }
6710
6711 if ( is_multisite() ) {
6712 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6713
6714 $this->do_action( "after_{$name}_cron_multisite" );
6715 }
6716 }
6717
6718 #endregion
6719
6720 #----------------------------------------------------------------------------------
6721 #region Daily Sync Cron
6722 #----------------------------------------------------------------------------------
6723
6724
6725 /**
6726 * @author Vova Feldman (@svovaf)
6727 * @since 2.0.0
6728 *
6729 * @return bool
6730 */
6731 private function is_sync_cron_scheduled() {
6732 return $this->is_cron_on( 'sync' );
6733 }
6734
6735 /**
6736 * Get the sync cron's executing blog ID.
6737 *
6738 * @author Vova Feldman (@svovaf)
6739 * @since 2.0.0
6740 *
6741 * @return int
6742 */
6743 private function get_sync_cron_blog_id() {
6744 return $this->get_cron_blog_id( 'sync' );
6745 }
6746
6747 /**
6748 * @author Vova Feldman (@svovaf)
6749 * @since 1.1.7.3
6750 */
6751 private function run_manual_sync() {
6752 self::require_pluggable_essentials();
6753
6754 if ( ! $this->is_user_admin() ) {
6755 return;
6756 }
6757
6758 // Run manual sync.
6759 $this->_sync_cron();
6760
6761 // Reschedule next cron to run 24 hours from now (performance optimization).
6762 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6763 }
6764
6765 /**
6766 * Data sync cron job. Replaces the background sync non blocking HTTP request
6767 * that doesn't halt page loading.
6768 *
6769 * @author Vova Feldman (@svovaf)
6770 * @since 1.1.7.3
6771 * @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.
6772 */
6773 function _sync_cron() {
6774 $this->_logger->entrance();
6775
6776 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6777 }
6778
6779 /**
6780 * The actual data sync cron logic.
6781 *
6782 * @author Vova Feldman (@svovaf)
6783 * @since 2.0.0
6784 *
6785 * @param int[] $blog_ids
6786 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6787 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6788 * updates for a single site in case `execute_cron` has switched to a different blog.
6789 */
6790 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6791 if ( $this->is_registered() ) {
6792 $this->sync_user_beta_mode();
6793
6794 if ( $this->has_paid_plan() ) {
6795 // Initiate background plan sync.
6796 $this->_sync_license( true, false, $current_blog_id );
6797
6798 if ( $this->is_paying() ) {
6799 // Check for premium plugin updates.
6800 $this->check_updates( true );
6801 }
6802 } else {
6803 // Sync install(s) (only if something changed locally).
6804 if ( 1 < count( $blog_ids ) ) {
6805 $this->sync_installs();
6806 } else {
6807 $this->sync_install();
6808 }
6809
6810 $this->maybe_sync_install_user();
6811 }
6812 }
6813 }
6814
6815 /**
6816 * Check if sync was executed in the last $period of seconds.
6817 *
6818 * @author Vova Feldman (@svovaf)
6819 * @since 1.1.7.3
6820 *
6821 * @param int $period In seconds
6822 *
6823 * @return bool
6824 */
6825 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6826 return $this->is_cron_executed( 'sync', $period );
6827 }
6828
6829 /**
6830 * @author Vova Feldman (@svovaf)
6831 * @since 1.1.7.3
6832 *
6833 * @return bool
6834 */
6835 private function is_sync_cron_on() {
6836 return $this->is_cron_on( 'sync' );
6837 }
6838
6839 /**
6840 * @author Vova Feldman (@svovaf)
6841 * @since 1.1.7.3
6842 *
6843 * @param int $start_at Defaults to now.
6844 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6845 * @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.
6846 */
6847 private function schedule_sync_cron(
6848 $start_at = WP_FS__SCRIPT_START_TIME,
6849 $randomize_start = true,
6850 $except_blog_id = 0
6851 ) {
6852 $this->schedule_cron(
6853 'sync',
6854 'data_sync',
6855 'daily',
6856 $start_at,
6857 $randomize_start,
6858 $except_blog_id
6859 );
6860 }
6861
6862 /**
6863 * Add the actual sync function to the cron job hook.
6864 *
6865 * @author Vova Feldman (@svovaf)
6866 * @since 1.1.7.3
6867 */
6868 private function hook_callback_to_sync_cron() {
6869 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6870 }
6871
6872 /**
6873 * @author Vova Feldman (@svovaf)
6874 * @since 1.1.7.3
6875 *
6876 * @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.
6877 */
6878 private function clear_sync_cron( $is_network_clear = false ) {
6879 $this->_logger->entrance();
6880
6881 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6882 }
6883
6884 /**
6885 * Unix timestamp for next sync cron execution or false if not scheduled.
6886 *
6887 * @author Vova Feldman (@svovaf)
6888 * @since 1.1.7.3
6889 *
6890 * @return int|false
6891 */
6892 function next_sync_cron() {
6893 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6894 }
6895
6896 /**
6897 * Unix timestamp for previous sync cron execution or false if never executed.
6898 *
6899 * @author Vova Feldman (@svovaf)
6900 * @since 1.1.7.3
6901 *
6902 * @return int|false
6903 */
6904 function last_sync_cron() {
6905 return $this->cron_last_execution( 'sync' );
6906 }
6907
6908 #endregion Daily Sync Cron ------------------------------------------------------------------
6909
6910 #----------------------------------------------------------------------------------
6911 #region Async Install Sync
6912 #----------------------------------------------------------------------------------
6913
6914 /**
6915 * @author Vova Feldman (@svovaf)
6916 * @since 1.1.7.3
6917 *
6918 * @return bool
6919 */
6920 private function is_install_sync_scheduled() {
6921 return $this->is_cron_on( 'install_sync' );
6922 }
6923
6924 /**
6925 * Get the sync cron's executing blog ID.
6926 *
6927 * @author Vova Feldman (@svovaf)
6928 * @since 2.0.0
6929 *
6930 * @return int
6931 */
6932 private function get_install_sync_cron_blog_id() {
6933 return $this->get_cron_blog_id( 'install_sync' );
6934 }
6935
6936 /**
6937 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6938 *
6939 * @author Vova Feldman (@svovaf)
6940 * @since 1.1.7.3
6941 *
6942 * @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.
6943 */
6944 private function schedule_install_sync( $except_blog_id = 0 ) {
6945 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6946 }
6947
6948 /**
6949 * Unix timestamp for previous install sync cron execution or false if never executed.
6950 *
6951 * @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.
6952 *
6953 * @author Vova Feldman (@svovaf)
6954 * @since 1.1.7.3
6955 *
6956 * @return int|false
6957 */
6958 function last_install_sync() {
6959 return $this->cron_last_execution( 'install_sync' );
6960 }
6961
6962 /**
6963 * Unix timestamp for next install sync cron execution or false if not scheduled.
6964 *
6965 * @author Vova Feldman (@svovaf)
6966 * @since 1.1.7.3
6967 *
6968 * @return int|false
6969 */
6970 function next_install_sync() {
6971 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6972 }
6973
6974 /**
6975 * Add the actual install sync function to the cron job hook.
6976 *
6977 * @author Vova Feldman (@svovaf)
6978 * @since 1.1.7.3
6979 */
6980 private function hook_callback_to_install_sync() {
6981 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6982 }
6983
6984 /**
6985 * @author Vova Feldman (@svovaf)
6986 * @since 1.1.7.3
6987 *
6988 * @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.
6989 */
6990 private function clear_install_sync_cron( $is_network_clear = false ) {
6991 $this->_logger->entrance();
6992
6993 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6994 }
6995
6996 /**
6997 * @author Vova Feldman (@svovaf)
6998 * @since 1.1.7.3
6999 * @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.
7000 */
7001 public function _run_sync_install() {
7002 $this->_logger->entrance();
7003
7004 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
7005 }
7006
7007 /**
7008 * The actual install(s) sync cron logic.
7009 *
7010 * @author Vova Feldman (@svovaf)
7011 * @since 2.0.0
7012 *
7013 * @param int[] $blog_ids
7014 * @param int|null $current_blog_id
7015 */
7016 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
7017 if ( $this->is_registered() ) {
7018 if ( 1 < count( $blog_ids ) ) {
7019 $this->sync_installs( array(), true );
7020 } else {
7021 $this->sync_install( array(), true );
7022 }
7023
7024 $this->maybe_sync_install_user();
7025 }
7026 }
7027
7028 #endregion Async Install Sync ------------------------------------------------------------------
7029
7030 /**
7031 * Show a notice that activation is currently pending.
7032 *
7033 * @author Vova Feldman (@svovaf)
7034 * @since 1.0.7
7035 *
7036 * @param bool|string $email
7037 * @param bool $is_pending_trial Since 1.2.1.5
7038 */
7039 function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) {
7040 if ( ! is_string( $email ) ) {
7041 $current_user = self::_get_current_wp_user();
7042 $email = $current_user->user_email;
7043 }
7044
7045 $this->_admin_notices->add_sticky(
7046 sprintf(
7047 $this->get_text_inline( 'You should receive an activation email for %s to your mailbox at %s. Please make sure you click the activation button in that email to %s.', 'pending-activation-message' ),
7048 '<b>' . $this->get_plugin_name() . '</b>',
7049 '<b>' . $email . '</b>',
7050 ( $is_pending_trial ?
7051 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7052 $this->get_text_inline( 'complete the install', 'complete-the-install' ) )
7053 ),
7054 'activation_pending',
7055 'Thanks!'
7056 );
7057 }
7058
7059 /**
7060 * Check if currently in plugin activation.
7061 *
7062 * @author Vova Feldman (@svovaf)
7063 * @since 1.1.4
7064 *
7065 * @return bool
7066 */
7067 function is_plugin_activation() {
7068 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7069
7070 return !empty($result);
7071 }
7072
7073 /**
7074 *
7075 * NOTE: admin_menu action executed before admin_init.
7076 *
7077 * @author Vova Feldman (@svovaf)
7078 * @since 1.0.7
7079 */
7080 function _admin_init_action() {
7081 /**
7082 * Automatically redirect to connect/activation page after plugin activation.
7083 *
7084 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7085 */
7086 if ( $this->is_plugin_activation() ) {
7087 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7088
7089 if ( isset( $_GET['activate-multi'] ) ) {
7090 /**
7091 * Don't redirect if activating multiple plugins at once (bulk activation).
7092 */
7093 } else {
7094 $this->_redirect_on_activation_hook();
7095 return;
7096 }
7097 }
7098
7099 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7100 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7101
7102 $this->skip_connection( null, fs_is_network_admin() );
7103
7104 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7105 }
7106
7107 if ( $this->is_network_activation_mode() &&
7108 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7109 ) {
7110 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7111
7112 $this->delegate_connection();
7113
7114 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7115 }
7116
7117 $this->_add_upgrade_action_link();
7118
7119 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7120 (
7121 ( true === $this->_storage->require_license_activation ) ||
7122 // Not registered nor anonymous.
7123 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7124 // OR, network level and in network upgrade mode.
7125 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7126 )
7127 ) {
7128 if ( ! $this->is_pending_activation() ) {
7129 if ( ! $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
7130 /**
7131 * If a user visits any other admin page before activating the premium-only theme with a valid
7132 * license, reactivate the previous theme.
7133 *
7134 * @author Leo Fajardo (@leorw)
7135 * @since 1.2.2
7136 */
7137 if ( $this->is_theme() &&
7138 ! $this->has_settings_menu() &&
7139 ! isset( $_REQUEST['fs_action'] ) &&
7140 $this->can_activate_previous_theme()
7141 ) {
7142 if ( $this->is_only_premium() ) {
7143 $this->activate_previous_theme();
7144 return;
7145 }
7146
7147 if ( true === $this->_storage->require_license_activation ) {
7148 $this->_storage->require_license_activation = false;
7149 }
7150 }
7151
7152 if ( ! fs_is_network_admin() &&
7153 $this->is_network_activation_mode() &&
7154 ! $this->is_delegated_connection()
7155 ) {
7156 return;
7157 }
7158
7159 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7160 if ( ! $this->_anonymous_mode ) {
7161 // Show notice for new plugin installations.
7162 $this->_admin_notices->add(
7163 sprintf(
7164 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7165 sprintf( '<b><a href="%s">%s</a></b>',
7166 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7167 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7168 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7169 )
7170 ),
7171 '',
7172 'update-nag'
7173 );
7174 }
7175 } else {
7176 if ( $this->should_add_sticky_optin_notice() ) {
7177 $this->add_sticky_optin_admin_notice();
7178 }
7179
7180 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7181 // Don't show admin nag if plugin update.
7182 wp_enqueue_script( 'wp-pointer' );
7183 wp_enqueue_style( 'wp-pointer' );
7184
7185 $this->_enqueue_connect_essentials();
7186
7187 add_action( 'admin_print_footer_scripts', array(
7188 $this,
7189 '_add_connect_pointer_script'
7190 ) );
7191 }
7192 }
7193 }
7194 }
7195
7196 if ( $this->show_opt_in_on_themes_page() &&
7197 $this->is_activation_page()
7198 ) {
7199 $this->_show_theme_activation_optin_dialog();
7200 }
7201 }
7202 }
7203
7204 /**
7205 * @author Vova Feldman (@svovaf)
7206 * @since 2.0.0
7207 *
7208 * @return bool
7209 */
7210 private function should_add_sticky_optin_notice() {
7211 if ( fs_is_network_admin() ) {
7212 if ( ! $this->_is_network_active ) {
7213 return false;
7214 }
7215
7216 if ( ! $this->is_network_activation_mode() ) {
7217 return false;
7218 }
7219
7220 return ! isset( $this->_storage->sticky_optin_added_ms );
7221 }
7222
7223 if ( ! $this->is_activation_mode() ) {
7224 return false;
7225 }
7226
7227 // If running from a blog admin and delegated the connection.
7228 return ! isset( $this->_storage->sticky_optin_added );
7229 }
7230
7231 /**
7232 * @author Leo Fajardo (@leorw)
7233 * @since 2.0.0
7234 */
7235 private function add_sticky_optin_admin_notice() {
7236 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7237 $this->_storage->sticky_optin_added = true;
7238 } else {
7239 $this->_storage->sticky_optin_added_ms = true;
7240 }
7241
7242 // Show notice for new plugin installations.
7243 $this->_admin_notices->add_sticky(
7244 sprintf(
7245 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7246 $this->_module_type,
7247 sprintf( '<b><a href="%s">%s</a></b>',
7248 $this->get_activation_url(),
7249 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7250 )
7251 ),
7252 'connect_account',
7253 '',
7254 'update-nag'
7255 );
7256 }
7257
7258 /**
7259 * Enqueue connect requires scripts and styles.
7260 *
7261 * @author Vova Feldman (@svovaf)
7262 * @since 1.1.4
7263 */
7264 function _enqueue_connect_essentials() {
7265 wp_enqueue_script( 'jquery' );
7266 wp_enqueue_script( 'json2' );
7267
7268 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7269 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7270
7271 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
7272 }
7273
7274 /**
7275 * Add connect / opt-in pointer.
7276 *
7277 * @author Vova Feldman (@svovaf)
7278 * @since 1.1.4
7279 */
7280 function _add_connect_pointer_script() {
7281 $vars = array( 'id' => $this->_module_id );
7282 $pointer_content = fs_get_template( 'connect.php', $vars );
7283 ?>
7284 <script type="text/javascript">// <![CDATA[
7285 jQuery(document).ready(function ($) {
7286 if ('undefined' !== typeof(jQuery().pointer)) {
7287
7288 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7289
7290 if (element.length > 0) {
7291 var optin = $(element).pointer($.extend(true, {}, {
7292 content : <?php echo json_encode( $pointer_content ) ?>,
7293 position : {
7294 edge : 'left',
7295 align: 'center'
7296 },
7297 buttons : function () {
7298 // Don't show pointer buttons.
7299 return '';
7300 },
7301 pointerWidth: 482
7302 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7303
7304 <?php
7305 echo $this->apply_filters( 'optin_pointer_execute', "
7306
7307 optin.pointer('open');
7308
7309 // Tag the opt-in pointer with custom class.
7310 $('.wp-pointer #fs_connect')
7311 .parents('.wp-pointer.wp-pointer-top')
7312 .addClass('fs-opt-in-pointer');
7313
7314 ", 'element', 'optin' ) ?>
7315 }
7316 }
7317 });
7318 // ]]></script>
7319 <?php
7320 }
7321
7322 /**
7323 * Return current page's URL.
7324 *
7325 * @author Vova Feldman (@svovaf)
7326 * @since 1.0.7
7327 *
7328 * @return string
7329 */
7330 function current_page_url() {
7331 $url = 'http';
7332
7333 if ( isset( $_SERVER["HTTPS"] ) ) {
7334 if ( $_SERVER["HTTPS"] == "on" ) {
7335 $url .= "s";
7336 }
7337 }
7338 $url .= "://";
7339 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7340 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7341 } else {
7342 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7343 }
7344
7345 return esc_url( $url );
7346 }
7347
7348 /**
7349 * Check if the current page is the plugin's main admin settings page.
7350 *
7351 * @author Vova Feldman (@svovaf)
7352 * @since 1.0.7
7353 *
7354 * @return bool
7355 */
7356 function _is_plugin_page() {
7357 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7358 fs_is_plugin_page( $this->_slug );
7359 }
7360
7361 /* Events
7362 ------------------------------------------------------------------------------------------------------------------*/
7363 /**
7364 * Delete site install from Database.
7365 *
7366 * @author Vova Feldman (@svovaf)
7367 * @since 1.0.1
7368 *
7369 * @param bool $store
7370 * @param int|null $blog_id Since 2.0.0
7371 *
7372 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7373 */
7374 function _delete_site( $store = true, $blog_id = null ) {
7375 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7376 }
7377
7378 /**
7379 * Delete site install from Database.
7380 *
7381 * @author Vova Feldman (@svovaf)
7382 * @since 1.2.2.7
7383 *
7384 * @param string $slug
7385 * @param string $module_type
7386 * @param bool $store
7387 * @param int|null $blog_id Since 2.0.0
7388 *
7389 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7390 */
7391 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7392 $sites = self::get_all_sites( $module_type, $blog_id );
7393
7394 $install_id = false;
7395
7396 if ( isset( $sites[ $slug ] ) ) {
7397 if ( is_object( $sites[ $slug ] ) ) {
7398 $install_id = $sites[ $slug ]->id;
7399 }
7400
7401 unset( $sites[ $slug ] );
7402
7403 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7404 }
7405
7406 return $install_id;
7407 }
7408
7409 /**
7410 * Delete user.
7411 *
7412 * @author Vova Feldman (@svovaf)
7413 * @since 2.0.0
7414 *
7415 * @param number $user_id
7416 * @param bool $store
7417 *
7418 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7419 */
7420 private static function delete_user( $user_id, $store = true ) {
7421 $users = self::get_all_users();
7422
7423 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7424 return false;
7425 }
7426
7427 unset( $users[ $user_id ] );
7428
7429 self::$_accounts->set_option( 'users', $users, $store );
7430
7431 return $user_id;
7432 }
7433
7434 /**
7435 * Delete plugin's plans information.
7436 *
7437 * @param bool $store Flush to Database if true.
7438 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7439 *
7440 * @author Vova Feldman (@svovaf)
7441 * @since 1.0.9
7442 */
7443 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7444 $this->_logger->entrance();
7445
7446 $plans = self::get_all_plans( $this->_module_type );
7447
7448 $plans_to_keep = array();
7449
7450 if ( $keep_associated_plans ) {
7451 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7452 foreach ( $plans_ids_to_keep as $plan_id ) {
7453 $plan = self::_get_plan_by_id( $plan_id );
7454 if ( is_object( $plan ) ) {
7455 $plans_to_keep[] = self::_encrypt_entity( $plan );
7456 }
7457 }
7458 }
7459
7460 if ( ! empty( $plans_to_keep ) ) {
7461 $plans[ $this->_slug ] = $plans_to_keep;
7462 } else {
7463 unset( $plans[ $this->_slug ] );
7464 }
7465
7466 $this->set_account_option( 'plans', $plans, $store );
7467 }
7468
7469 /**
7470 * Delete all plugin licenses.
7471 *
7472 * @author Vova Feldman (@svovaf)
7473 * @since 1.0.9
7474 *
7475 * @param bool $store
7476 */
7477 private function _delete_licenses( $store = true ) {
7478 $this->_logger->entrance();
7479
7480 $all_licenses = self::get_all_licenses();
7481
7482 unset( $all_licenses[ $this->_module_id ] );
7483
7484 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7485 }
7486
7487 /**
7488 * Check if Freemius was added on new plugin installation.
7489 *
7490 * @author Vova Feldman (@svovaf)
7491 * @since 1.1.5
7492 *
7493 * @return bool
7494 */
7495 function is_plugin_new_install() {
7496 return isset( $this->_storage->is_plugin_new_install ) &&
7497 $this->_storage->is_plugin_new_install;
7498 }
7499
7500 /**
7501 * Check if it's the first plugin release that is running Freemius.
7502 *
7503 * @author Vova Feldman (@svovaf)
7504 * @since 1.2.1.5
7505 *
7506 * @return bool
7507 */
7508 function is_first_freemius_powered_version() {
7509 return empty( $this->_storage->plugin_last_version );
7510 }
7511
7512 /**
7513 * @author Leo Fajardo (@leorw)
7514 * @since 1.2.2
7515 *
7516 * @return bool|string
7517 */
7518 private function get_previous_theme_slug() {
7519 return isset( $this->_storage->previous_theme ) ?
7520 $this->_storage->previous_theme :
7521 false;
7522 }
7523
7524 /**
7525 * @author Leo Fajardo (@leorw)
7526 * @since 1.2.2
7527 *
7528 * @return string
7529 */
7530 private function can_activate_previous_theme() {
7531 $slug = $this->get_previous_theme_slug();
7532 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7533 $theme_instance = wp_get_theme( $slug );
7534
7535 return $theme_instance->exists();
7536 }
7537
7538 return false;
7539 }
7540
7541 /**
7542 * @author Leo Fajardo (@leorw)
7543 * @since 1.2.2
7544 */
7545 private function activate_previous_theme() {
7546 switch_theme( $this->get_previous_theme_slug() );
7547 unset( $this->_storage->previous_theme );
7548
7549 global $pagenow;
7550 if ( 'themes.php' === $pagenow ) {
7551 /**
7552 * Refresh the active theme information.
7553 *
7554 * @author Leo Fajardo (@leorw)
7555 * @since 1.2.2
7556 */
7557 fs_redirect( $this->admin_url( $pagenow ) );
7558 }
7559 }
7560
7561 /**
7562 * @author Leo Fajardo (@leorw)
7563 * @since 1.2.2
7564 *
7565 * @return string
7566 */
7567 function get_previous_theme_activation_url() {
7568 if ( ! $this->can_activate_previous_theme() ) {
7569 return '';
7570 }
7571
7572 /**
7573 * Activation URL
7574 *
7575 * @author Leo Fajardo (@leorw)
7576 * @since 1.2.2
7577 */
7578 return wp_nonce_url(
7579 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7580 'switch-theme_' . $this->get_previous_theme_slug()
7581 );
7582 }
7583
7584 /**
7585 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7586 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7587 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7588 * theme doesn't exist, then there will be no close button.
7589 *
7590 * @author Leo Fajardo (@leorw)
7591 * @since 1.2.2
7592 *
7593 * @param string $slug_or_name Old theme's slug or name.
7594 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7595 */
7596 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7597 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7598 $old_theme->get_stylesheet() :
7599 $slug_or_name;
7600
7601 $this->_activate_plugin_event_hook();
7602 }
7603
7604 /**
7605 * Plugin activated hook.
7606 *
7607 * @author Vova Feldman (@svovaf)
7608 * @since 1.0.1
7609 *
7610 * @uses FS_Api
7611 */
7612 function _activate_plugin_event_hook() {
7613 $this->_logger->entrance( 'slug = ' . $this->_slug );
7614
7615 if ( ! $this->is_user_admin() ) {
7616 return;
7617 }
7618
7619 $this->unregister_uninstall_hook();
7620
7621 // Clear API cache on activation.
7622 FS_Api::clear_cache();
7623
7624 $is_premium_version_activation = $this->is_plugin() ?
7625 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7626 $this->is_premium();
7627
7628 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7629
7630 if ( $this->is_plugin() ) {
7631 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7632 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7633 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7634 $other_version_basename = $is_premium_version_activation ?
7635 $this->_free_plugin_basename :
7636 $this->premium_plugin_basename();
7637
7638 if ( ! $this->_is_network_active ) {
7639 /**
7640 * Themes are always network activated, but the ACTUAL activation is per site.
7641 *
7642 * During the activation, the plugin isn't yet active, therefore,
7643 * _is_network_active will be set to false even if it's a network level
7644 * activation. So we need to fix that by looking at the is_network_admin() value.
7645 *
7646 * @author Vova Feldman
7647 */
7648 $this->_is_network_active = (
7649 $this->_is_multisite_integrated &&
7650 fs_is_network_admin()
7651 );
7652 }
7653
7654 /**
7655 * If the other module version is active, deactivate it.
7656 *
7657 * is_plugin_active() checks if the plugin is active on the site or the network level and
7658 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7659 *
7660 * @author Leo Fajardo (@leorw)
7661 * @since 1.2.2
7662 */
7663 if ( is_plugin_active( $other_version_basename ) ) {
7664 deactivate_plugins( $other_version_basename );
7665 }
7666 }
7667
7668 if ( $this->is_registered() ) {
7669 if ( $is_premium_version_activation ) {
7670 $this->reconnect_locally();
7671 }
7672
7673
7674 // Schedule re-activation event and sync.
7675 // $this->sync_install( array(), true );
7676 $this->schedule_install_sync();
7677
7678 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7679 if ( $is_premium_version_activation ) {
7680 $this->_admin_notices->add(
7681 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7682 $this->get_text_x_inline( 'W00t',
7683 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7684 );
7685 }
7686 } else if ( $this->is_anonymous() ) {
7687 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7688 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7689 $network = true;
7690 } else {
7691 $plugin_version = $this->_storage->is_anonymous['version'];
7692 $network = false;
7693 }
7694
7695 /**
7696 * Reset "skipped" click cache on the following:
7697 * 1. Freemius DEV mode.
7698 * 2. WordPress DEBUG mode.
7699 * 3. If a plugin and the user skipped the exact same version before.
7700 *
7701 * @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).
7702 *
7703 * @todo 4. If explicitly asked to retry after every activation.
7704 */
7705 if ( WP_FS__DEV_MODE ||
7706 (
7707 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7708 $this->get_plugin_version() == $plugin_version
7709 )
7710 ) {
7711 $this->reset_anonymous_mode( $network );
7712 }
7713 }
7714
7715 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7716
7717 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7718 /**
7719 * When activating an add-on, try to also activate a license.
7720 *
7721 * @author Leo Fajardo (@leorw)
7722 * @since 2.3.0
7723 */
7724 if ( ! $this->_is_network_active ) {
7725 $this->maybe_activate_addon_license();
7726 } else {
7727 $this->maybe_network_activate_addon_license();
7728 }
7729
7730 /**
7731 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7732 *
7733 * @author Leo Fajardo (@leorw)
7734 * @since 2.3.0
7735 */
7736 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7737
7738 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7739 $this->_storage->require_license_activation = false;
7740 }
7741 }
7742
7743 if (
7744 $is_premium_version_activation &&
7745 (
7746 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7747 (
7748 $this->is_registered() &&
7749 ! $is_trial_or_has_features_enabled_license
7750 )
7751 )
7752 ) {
7753 $this->_storage->require_license_activation = true;
7754 }
7755
7756 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7757 /**
7758 * If no previous version of plugin's version exist, it means that it's either
7759 * the first time that the plugin installed on the site, or the plugin was installed
7760 * before but didn't have Freemius integrated.
7761 *
7762 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7763 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7764 * only after immediate activation.
7765 *
7766 * @since 1.1.4
7767 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7768 */
7769 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7770 }
7771
7772 if ( ! $this->_anonymous_mode &&
7773 $this->has_api_connectivity( WP_FS__DEV_MODE ) &&
7774 ! $this->_isAutoInstall
7775 ) {
7776 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7777 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7778 }
7779
7780 /**
7781 * Activation hook is executed after the plugin's main file is loaded, therefore,
7782 * after the plugin was loaded. The logic is located at activate_plugin()
7783 * ./wp-admin/includes/plugin.php.
7784 *
7785 * @author Vova Feldman (@svovaf)
7786 * @since 1.1.9
7787 */
7788 $this->_storage->was_plugin_loaded = true;
7789 }
7790
7791 /**
7792 * @author Leo Fajardo (@leorw)
7793 * @since 2.3.0
7794 */
7795 private function maybe_activate_addon_license() {
7796 $parent_fs = $this->get_parent_instance();
7797
7798 if (
7799 ! is_object( $parent_fs ) ||
7800 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7801 ) {
7802 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7803 return;
7804 }
7805
7806 $license = $this->get_addon_active_parent_license();
7807 if ( ! is_object( $license ) ) {
7808 return;
7809 }
7810
7811 if ( ! $this->is_registered() ) {
7812 // Opt in with a license key.
7813 $this->opt_in(
7814 $parent_fs->get_current_or_network_user()->email,
7815 false,
7816 false,
7817 $license->secret_key
7818 );
7819 } else {
7820 // Activate the license.
7821 $install = $this->get_api_site_scope()->call(
7822 '/',
7823 'put',
7824 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7825 );
7826
7827 if ( ! FS_Api::is_api_error( $install ) ) {
7828 $this->_sync_addon_license( $this->get_id(), true );
7829 }
7830 }
7831 }
7832
7833
7834 /**
7835 * @author Leo Fajardo (@leorw)
7836 * @since 2.3.0
7837 *
7838 * @param FS_Plugin_License $license
7839 */
7840 private function maybe_network_activate_addon_license( $license = null ) {
7841 $parent_fs = $this->get_parent_instance();
7842 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7843 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7844 return;
7845 }
7846
7847 $license = ( ! is_null( $license ) ) ?
7848 $license :
7849 $this->get_addon_active_parent_license();
7850
7851 if ( ! is_object( $license ) ) {
7852 return;
7853 }
7854
7855 if ( ! $this->is_network_registered() ) {
7856 $sites = $this->get_sites_for_network_level_optin();
7857
7858 if ( count( $sites ) > $license->left() ) {
7859 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7860 return;
7861 }
7862
7863 // Opt in with a license key.
7864 $this->opt_in(
7865 $parent_fs->get_user()->email,
7866 false,
7867 false,
7868 $license->secret_key,
7869 false,
7870 false,
7871 false,
7872 null,
7873 $sites
7874 );
7875 } else {
7876 $blog_2_install_map = array();
7877 $site_ids = array();
7878
7879 $all_sites = Freemius::get_sites();
7880
7881 foreach ( $all_sites as $site ) {
7882 $blog_id = Freemius::get_site_blog_id( $site );
7883 $install = $this->get_install_by_blog_id( $blog_id );
7884
7885 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7886 // Skip license activation for installs that are already associated with a license.
7887 continue;
7888 }
7889
7890 if ( is_object( $install ) ) {
7891 $blog_2_install_map[ $blog_id ] = $install;
7892 } else {
7893 $site_ids[] = $blog_id;
7894 }
7895 }
7896
7897 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7898 return;
7899 }
7900
7901 $user = $this->get_current_or_network_user();
7902
7903 if ( ! empty( $blog_2_install_map ) ) {
7904 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7905
7906 if ( true !== $result ) {
7907 return;
7908 }
7909 }
7910
7911 if ( ! empty( $site_ids ) ) {
7912 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7913 }
7914 }
7915 }
7916
7917 /**
7918 * @author Leo Fajardo (@leorw)
7919 * @since 2.3.0
7920 *
7921 * @return FS_Plugin_License
7922 */
7923 private function get_addon_active_parent_license() {
7924 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
7925 $parent_instance = $this->get_parent_instance();
7926
7927 $foreign_licenses = $parent_instance->get_foreign_licenses_info(
7928 self::get_all_licenses( $this->get_parent_id() )
7929 );
7930
7931 if ( ! empty ( $foreign_licenses ) ) {
7932 $foreign_licenses = array(
7933 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
7934 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
7935 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
7936 );
7937
7938 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
7939 }
7940
7941 $result = $parent_instance->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, true );
7942
7943 if (
7944 ! $this->is_api_result_object( $result, 'licenses' ) ||
7945 ! is_array( $result->licenses ) ||
7946 empty( $result->licenses )
7947 ) {
7948 return null;
7949 }
7950
7951 $license = new FS_Plugin_License( $result->licenses[ 0 ] );
7952
7953 return $license;
7954 }
7955
7956 /**
7957 * @author Leo Fajardo (@leorw)
7958 * @since 2.3.0
7959 *
7960 * @return array
7961 */
7962 function get_sites_for_network_level_optin() {
7963 $sites = array();
7964 $all_sites = self::get_sites();
7965
7966 foreach ( $all_sites as $site ) {
7967 $blog_id = self::get_site_blog_id( $site );
7968
7969 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
7970 ! $this->is_installed_on_site( $blog_id )
7971 ) {
7972 $sites[] = $this->get_site_info( $site );
7973 }
7974 }
7975
7976 return $sites;
7977 }
7978
7979 /**
7980 * Delete account.
7981 *
7982 * @author Vova Feldman (@svovaf)
7983 * @since 1.0.3
7984 *
7985 * @param bool $check_user Enforce checking if user have plugins activation privileges.
7986 */
7987 function delete_account_event( $check_user = true ) {
7988 $this->_logger->entrance( 'slug = ' . $this->_slug );
7989
7990 if ( $check_user && ! $this->is_user_admin() ) {
7991 return;
7992 }
7993
7994 $this->do_action( 'before_account_delete' );
7995
7996 // Clear all admin notices.
7997 $this->_admin_notices->clear_all_sticky( false );
7998
7999 $this->_delete_site( false );
8000
8001 $delete_network_common_data = true;
8002
8003 if ( $this->_is_network_active ) {
8004 $installs = $this->get_blog_install_map();
8005
8006 // Don't delete common network data unless no other installs left.
8007 $delete_network_common_data = empty( $installs );
8008 }
8009
8010 if ( $delete_network_common_data ) {
8011 $this->_delete_plans( false );
8012
8013 $this->_delete_licenses( false );
8014
8015 // Delete add-ons related to plugin's account.
8016 $this->_delete_account_addons( false );
8017 }
8018
8019 // @todo Delete plans and licenses of add-ons.
8020
8021 self::$_accounts->store();
8022
8023 /**
8024 * IMPORTANT:
8025 * Clear crons must be executed before clearing all storage.
8026 * Otherwise, the cron will not be cleared.
8027 */
8028 if ( $delete_network_common_data ) {
8029 $this->clear_sync_cron();
8030 }
8031
8032 $this->clear_install_sync_cron();
8033
8034 // Clear all storage data.
8035 $this->_storage->clear_all( true, array(
8036 'is_delegated_connection',
8037 'connectivity_test',
8038 'is_on',
8039 ), false );
8040
8041 // Send delete event.
8042 $this->get_api_site_scope()->call( '/', 'delete' );
8043
8044 $this->do_action( 'after_account_delete' );
8045 }
8046
8047 /**
8048 * Delete network level account.
8049 *
8050 * @author Vova Feldman (@svovaf)
8051 * @since 2.0.0
8052 *
8053 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8054 */
8055 function delete_network_account_event( $check_user = true ) {
8056 $this->_logger->entrance( 'slug = ' . $this->_slug );
8057
8058 if ( $check_user && ! $this->is_user_admin() ) {
8059 return;
8060 }
8061
8062 $this->do_action( 'before_network_account_delete' );
8063
8064 // Clear all admin notices.
8065 $this->_admin_notices->clear_all_sticky();
8066
8067 $this->_delete_plans( false, false );
8068
8069 $this->_delete_licenses( false );
8070
8071 // Delete add-ons related to plugin's account.
8072 $this->_delete_account_addons( false );
8073
8074 // @todo Delete plans and licenses of add-ons.
8075
8076 self::$_accounts->store( true );
8077
8078 /**
8079 * IMPORTANT:
8080 * Clear crons must be executed before clearing all storage.
8081 * Otherwise, the cron will not be cleared.
8082 */
8083 $this->clear_sync_cron( true );
8084 $this->clear_install_sync_cron( true );
8085
8086 $sites = self::get_sites();
8087
8088 $install_ids = array();
8089 foreach ( $sites as $site ) {
8090 $blog_id = self::get_site_blog_id( $site );
8091
8092 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8093 continue;
8094 }
8095
8096 $install_id = $this->_delete_site( true, $blog_id );
8097
8098 // Clear all storage data.
8099 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8100
8101 if ( FS_Site::is_valid_id( $install_id ) ) {
8102 $install_ids[] = $install_id;
8103 }
8104
8105 switch_to_blog( $blog_id );
8106
8107 $this->do_action( 'after_account_delete' );
8108
8109 restore_current_blog();
8110 }
8111
8112 $this->_storage->clear_all( true, array(
8113 'connectivity_test',
8114 'is_on',
8115 ), true );
8116
8117 // Send delete event.
8118 if ( ! empty( $install_ids ) ) {
8119 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8120 }
8121
8122 $this->do_action( 'after_network_account_delete' );
8123 }
8124
8125 /**
8126 * Plugin deactivation hook.
8127 *
8128 * @author Vova Feldman (@svovaf)
8129 * @since 1.0.1
8130 */
8131 function _deactivate_plugin_hook() {
8132 $this->_logger->entrance( 'slug = ' . $this->_slug );
8133
8134 if ( ! $this->is_user_admin() ) {
8135 return;
8136 }
8137
8138 $is_network_deactivation = fs_is_network_admin();
8139 $storage_keys_for_removal = array();
8140
8141 $this->_admin_notices->clear_all_sticky();
8142
8143 $storage_keys_for_removal[] = 'sticky_optin_added';
8144 if ( isset( $this->_storage->sticky_optin_added ) ) {
8145 unset( $this->_storage->sticky_optin_added );
8146 }
8147
8148 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8149 // Remember that plugin was already installed.
8150 $this->_storage->is_plugin_new_install = false;
8151 }
8152
8153 // Hook to plugin uninstall.
8154 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8155
8156 $this->clear_module_main_file_cache();
8157 $this->clear_sync_cron( $this->_is_network_active );
8158 $this->clear_install_sync_cron();
8159
8160 if ( $this->is_registered() ) {
8161 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8162 FS_Plugin_Updater::instance( $this )->delete_update_data();
8163 }
8164
8165 if ( $is_network_deactivation ) {
8166 // Send deactivation event.
8167 $this->sync_installs( array(
8168 'is_active' => false,
8169 ) );
8170 } else {
8171 // Send deactivation event.
8172 $this->sync_install( array(
8173 'is_active' => false,
8174 ) );
8175 }
8176 } else {
8177 if ( ! $this->has_api_connectivity() ) {
8178 // Reset connectivity test cache.
8179 unset( $this->_storage->connectivity_test );
8180
8181 $storage_keys_for_removal[] = 'connectivity_test';
8182 }
8183 }
8184
8185 if ( $is_network_deactivation ) {
8186 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8187 unset( $this->_storage->sticky_optin_added_ms );
8188 }
8189
8190 if ( ! empty( $storage_keys_for_removal ) ) {
8191 $sites = self::get_sites();
8192
8193 foreach ( $sites as $site ) {
8194 $blog_id = self::get_site_blog_id( $site );
8195
8196 foreach ( $storage_keys_for_removal as $key ) {
8197 $this->_storage->remove( $key, false, $blog_id );
8198 }
8199
8200 $this->_storage->save( $blog_id );
8201 }
8202 }
8203 }
8204
8205 // Clear API cache on deactivation.
8206 FS_Api::clear_cache();
8207
8208 $this->remove_sdk_reference();
8209 }
8210
8211 /**
8212 * @author Vova Feldman (@svovaf)
8213 * @since 1.1.6
8214 */
8215 private function remove_sdk_reference() {
8216 global $fs_active_plugins;
8217
8218 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8219 if ( $this->_plugin_basename == $data->plugin_path ) {
8220 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8221 break;
8222 }
8223 }
8224
8225 fs_fallback_to_newest_active_sdk();
8226 }
8227
8228 /**
8229 * @author Vova Feldman (@svovaf)
8230 * @since 1.1.3
8231 *
8232 * @param bool $is_anonymous
8233 * @param bool|int $network_or_blog_id Since 2.0.0
8234 */
8235 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8236 // Store information regarding skip to try and opt-in the user
8237 // again in the future.
8238 $skip_info = array(
8239 'is' => $is_anonymous,
8240 'timestamp' => WP_FS__SCRIPT_START_TIME,
8241 'version' => $this->get_plugin_version(),
8242 );
8243
8244 if ( true === $network_or_blog_id ) {
8245 $this->_storage->is_anonymous_ms = $skip_info;
8246 } else {
8247 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8248 }
8249
8250 $this->network_upgrade_mode_completed();
8251
8252 // Update anonymous mode cache.
8253 $this->_is_anonymous = $is_anonymous;
8254 }
8255
8256 /**
8257 * @author Vova Feldman (@svovaf)
8258 * @since 2.0.0
8259 *
8260 * @param int $blog_id Site ID.
8261 * @param int $user_id User ID.
8262 * @param string $domain Site domain.
8263 * @param string $path Site path.
8264 * @param int $network_id Network ID. Only relevant on multi-network installations.
8265 * @param array $meta Metadata. Used to set initial site options.
8266 *
8267 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8268 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8269 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8270 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8271 */
8272 function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8273 $this->_logger->entrance();
8274
8275 if ( $this->is_premium() &&
8276 $this->is_network_connected() &&
8277 is_object( $this->_license ) &&
8278 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8279 $this->is_license_network_active( $blog_id )
8280 ) {
8281 /**
8282 * 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.
8283 */
8284 $current_blog_id = get_current_blog_id();
8285 $license = clone $this->_license;
8286
8287 $this->switch_to_blog( $blog_id );
8288
8289 // Opt-in with network user.
8290 $this->install_with_user(
8291 $this->get_network_user(),
8292 $license->secret_key,
8293 false,
8294 false,
8295 false
8296 );
8297
8298 if ( is_object( $this->_site ) ) {
8299 if ( $this->_site->license_id == $license->id ) {
8300 /**
8301 * If the license was activated successfully, sync the license data from the remote server.
8302 */
8303 $this->_license = $license;
8304 $this->sync_site_license();
8305 }
8306 }
8307
8308 $this->switch_to_blog( $current_blog_id );
8309
8310 if ( is_object( $this->_site ) ) {
8311 // Already connected (with or without a license), so no need to continue.
8312 return;
8313 }
8314 }
8315
8316 if ( $this->is_network_anonymous() ) {
8317 /**
8318 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8319 */
8320 $this->skip_site_connection( $blog_id );
8321 } else if ( $this->is_network_delegated_connection() ) {
8322 /**
8323 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8324 */
8325 $this->delegate_site_connection( $blog_id );
8326 } else if ( $this->is_network_connected() ) {
8327 /**
8328 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8329 */
8330 $current_blog_id = get_current_blog_id();
8331
8332 $this->switch_to_blog( $blog_id );
8333
8334 // Opt-in with network user.
8335 $this->install_with_user(
8336 $this->get_network_user(),
8337 false,
8338 false,
8339 false,
8340 false
8341 );
8342
8343 $this->switch_to_blog( $current_blog_id );
8344 } else {
8345 /**
8346 * If the super-admin mixed different options (connect, skip, delegated):
8347 * a) If at least one site connection was delegated, then automatically delegate connection.
8348 * 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.
8349 */
8350 $has_delegated_site = false;
8351
8352 $sites = self::get_sites();
8353 foreach ( $sites as $site ) {
8354 $blog_id = self::get_site_blog_id( $site );
8355
8356 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8357 $has_delegated_site = true;
8358 break;
8359 }
8360 }
8361
8362 if ( $has_delegated_site ) {
8363 $this->delegate_site_connection( $blog_id );
8364 } else {
8365 $this->skip_site_connection( $blog_id );
8366 }
8367 }
8368 }
8369
8370 /**
8371 * @author Vova Feldman (@svovaf)
8372 * @since 1.1.3
8373 *
8374 * @param bool|int $network_or_blog_id Since 2.0.0.
8375 */
8376 private function reset_anonymous_mode( $network_or_blog_id = 0 ) {
8377 if ( true === $network_or_blog_id ) {
8378 unset( $this->_storage->is_anonymous_ms );
8379 } else {
8380 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8381 }
8382
8383 /**
8384 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8385 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8386 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8387 *
8388 * @author Leo Fajardo (@leorw)
8389 * @since 1.2.2
8390 */
8391 if ( ! $this->_is_network_active ||
8392 0 === $network_or_blog_id ||
8393 get_current_blog_id() == $network_or_blog_id ||
8394 ( true === $network_or_blog_id && fs_is_network_admin() )
8395 ) {
8396 $this->_is_anonymous = null;
8397 }
8398 }
8399
8400 /**
8401 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8402 * deleting the account in the network level, the URL of the page to redirect to is correct.
8403 *
8404 * @author Leo Fajardo (@leorw)
8405 *
8406 * @since 2.1.3
8407 */
8408 private function maybe_set_slug_and_network_menu_exists_flag() {
8409 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8410 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8411 $this->_menu->get_slug() :
8412 $this->_slug
8413 );
8414 }
8415 }
8416
8417 /**
8418 * Clears the anonymous mode and redirects to the opt-in screen.
8419 *
8420 * @author Vova Feldman (@svovaf)
8421 * @since 1.1.7
8422 */
8423 function connect_again() {
8424 if ( ! $this->is_anonymous() ) {
8425 return;
8426 }
8427
8428 $this->reset_anonymous_mode( fs_is_network_admin() );
8429
8430 $this->maybe_set_slug_and_network_menu_exists_flag();
8431
8432 fs_redirect( $this->get_activation_url() );
8433 }
8434
8435 /**
8436 * Skip account connect, and set anonymous mode.
8437 *
8438 * @author Vova Feldman (@svovaf)
8439 * @since 1.1.1
8440 *
8441 * @param array|null $sites Since 2.0.0. Specific sites.
8442 * @param bool $skip_all_network Since 2.0.0. If true, skip connection for all sites.
8443 */
8444 function skip_connection( $sites = null, $skip_all_network = false ) {
8445 $this->_logger->entrance();
8446
8447 $this->_admin_notices->remove_sticky( 'connect_account' );
8448
8449 if ( $skip_all_network ) {
8450 $this->set_anonymous_mode( true, true );
8451 }
8452
8453 if ( ! $skip_all_network && empty( $sites ) ) {
8454 $this->skip_site_connection();
8455 } else {
8456 $uids = array();
8457
8458 if ( $skip_all_network ) {
8459 $this->set_anonymous_mode( true, true );
8460
8461 $sites = self::get_sites();
8462 foreach ( $sites as $site ) {
8463 $blog_id = self::get_site_blog_id( $site );
8464 $this->skip_site_connection( $blog_id, false );
8465 $uids[] = $this->get_anonymous_id( $blog_id );
8466 }
8467 } else if ( ! empty( $sites ) ) {
8468 foreach ( $sites as $site ) {
8469 $uids[] = $site['uid'];
8470 $this->skip_site_connection( $site['blog_id'], false );
8471 }
8472 }
8473
8474 // Send anonymous skip event.
8475 // No user identified info nor any tracking will be sent after the user skips the opt-in.
8476 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8477 'uids' => $uids,
8478 ) );
8479 }
8480
8481 $this->network_upgrade_mode_completed();
8482 }
8483
8484 /**
8485 * Skip connection for specific site in the network.
8486 *
8487 * @author Vova Feldman (@svovaf)
8488 * @since 2.0.0
8489 *
8490 * @param int|null $blog_id
8491 * @param bool $send_skip
8492 */
8493 private function skip_site_connection( $blog_id = null, $send_skip = true ) {
8494 $this->_logger->entrance();
8495
8496 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8497
8498 $this->set_anonymous_mode( true, $blog_id );
8499
8500 if ( $send_skip ) {
8501 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8502 'uids' => array( $this->get_anonymous_id( $blog_id ) ),
8503 ) );
8504 }
8505 }
8506
8507 /**
8508 * Plugin version update hook.
8509 *
8510 * @author Vova Feldman (@svovaf)
8511 * @since 1.0.4
8512 */
8513 private function update_plugin_version_event() {
8514 $this->_logger->entrance();
8515
8516 if ( ! $this->is_registered() ) {
8517 return;
8518 }
8519
8520 $this->schedule_install_sync();
8521 // $this->sync_install( array(), true );
8522 }
8523
8524 /**
8525 * Generate an MD5 signature of a plugins collection.
8526 * This helper methods used to identify changes in a plugins collection.
8527 *
8528 * @author Vova Feldman (@svovaf)
8529 * @since 2.0.0
8530 *
8531 * @param array [string]array $plugins
8532 *
8533 * @return string
8534 */
8535 private function get_plugins_thumbprint( $plugins ) {
8536 ksort( $plugins );
8537
8538 $thumbprint = '';
8539 foreach ( $plugins as $basename => $data ) {
8540 $thumbprint .= $data['slug'] . ',' .
8541 $data['Version'] . ',' .
8542 ( $data['is_active'] ? '1' : '0' ) . ';';
8543 }
8544
8545 return md5( $thumbprint );
8546 }
8547
8548 /**
8549 * Return a list of modified plugins since the last sync.
8550 *
8551 * Note:
8552 * There's no point to store a plugins counter since even if the number of
8553 * plugins didn't change, we still need to check if the versions are all the
8554 * same and the activity state is similar.
8555 *
8556 * @author Vova Feldman (@svovaf)
8557 * @since 1.1.8
8558 *
8559 * @return array|false
8560 */
8561 private function get_plugins_data_for_api() {
8562 // Alias.
8563 $site_active_plugins_option_name = 'active_plugins';
8564 $network_plugins_option_name = 'all_plugins';
8565
8566 /**
8567 * Collection of all site level active plugins.
8568 */
8569 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8570
8571 if ( ! is_object( $site_active_plugins_cache ) ) {
8572 $site_active_plugins_cache = (object) array(
8573 'timestamp' => '',
8574 'md5' => '',
8575 'plugins' => array(),
8576 );
8577 }
8578
8579 $time = time();
8580
8581 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8582 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8583 ) {
8584 // Don't send plugin updates if last update was in the past 5 min.
8585 return false;
8586 }
8587
8588 // Write timestamp to lock the logic.
8589 $site_active_plugins_cache->timestamp = $time;
8590 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8591
8592 // Reload options from DB.
8593 self::$_accounts->load( true );
8594 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8595
8596 if ( $time != $site_active_plugins_cache->timestamp ) {
8597 // If timestamp is different, then another thread captured the lock.
8598 return false;
8599 }
8600
8601 /**
8602 * Collection of all plugins (network level).
8603 */
8604 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8605
8606 if ( ! is_object( $network_plugins_cache ) ) {
8607 $network_plugins_cache = (object) array(
8608 'timestamp' => '',
8609 'md5' => '',
8610 'plugins' => array(),
8611 );
8612 }
8613
8614 // Check if there's a change in plugins.
8615 $network_plugins = self::get_network_plugins();
8616 $site_active_plugins = self::get_site_active_plugins();
8617
8618 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8619 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8620
8621 // Check if plugins status changed (version or active/inactive).
8622 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8623 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8624
8625 if ( ! $network_plugins_changed &&
8626 ! $site_active_plugins_changed
8627 ) {
8628 // No changes.
8629 return array();
8630 }
8631
8632 $plugins_update_data = array();
8633
8634 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8635 if ( ! isset( $network_plugins[ $basename ] ) ) {
8636 // Plugin uninstalled.
8637 $uninstalled_plugin_data = $data;
8638 $uninstalled_plugin_data['is_active'] = false;
8639 $uninstalled_plugin_data['is_uninstalled'] = true;
8640 $plugins_update_data[] = $uninstalled_plugin_data;
8641
8642 unset( $network_plugins[ $basename ] );
8643
8644 unset( $network_plugins_cache->plugins[ $basename ] );
8645 unset( $site_active_plugins_cache->plugins[ $basename ] );
8646
8647 continue;
8648 }
8649
8650 $was_active = $data['is_active'] ||
8651 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8652 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8653 $is_active = $network_plugins[ $basename ]['is_active'] ||
8654 ( isset( $site_active_plugins[ $basename ] ) &&
8655 $site_active_plugins[ $basename ]['is_active'] );
8656
8657 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8658 isset( $site_active_plugins[ $basename ] )
8659 ) {
8660 // Plugin was site level activated.
8661 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
8662 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8663 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8664 ! isset( $site_active_plugins[ $basename ] )
8665 ) {
8666 // Plugin was site level deactivated.
8667 unset( $site_active_plugins_cache->plugins[ $basename ] );
8668 }
8669
8670 $prev_version = $data['version'];
8671 $current_version = $network_plugins[ $basename ]['Version'];
8672
8673 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8674 // Plugin activated or deactivated, or version changed.
8675
8676 if ( $was_active !== $is_active ) {
8677 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8678 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8679 }
8680 }
8681
8682 if ( $prev_version !== $current_version ) {
8683 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8684 }
8685
8686 $updated_plugin_data = $data;
8687 $updated_plugin_data['is_active'] = $is_active;
8688 $updated_plugin_data['version'] = $current_version;
8689 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8690 $plugins_update_data[] = $updated_plugin_data;
8691 }
8692 }
8693
8694 // Find new plugins that weren't yet seen before.
8695 foreach ( $network_plugins as $basename => $data ) {
8696 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8697 // New plugin.
8698 $new_plugin = array(
8699 'slug' => $data['slug'],
8700 'version' => $data['Version'],
8701 'title' => $data['Name'],
8702 'is_active' => $data['is_active'],
8703 'is_uninstalled' => false,
8704 );
8705
8706 $plugins_update_data[] = $new_plugin;
8707 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8708
8709 if ( isset( $site_active_plugins[ $basename ] ) ) {
8710 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8711 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8712 }
8713 }
8714 }
8715
8716 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8717 $site_active_plugins_cache->timestamp = $time;
8718 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8719
8720 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8721 $network_plugins_cache->timestamp = $time;
8722 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8723
8724 return $plugins_update_data;
8725 }
8726
8727 /**
8728 * Return a list of modified themes since the last sync.
8729 *
8730 * Note:
8731 * There's no point to store a themes counter since even if the number of
8732 * themes didn't change, we still need to check if the versions are all the
8733 * same and the activity state is similar.
8734 *
8735 * @author Vova Feldman (@svovaf)
8736 * @since 1.1.8
8737 *
8738 * @return array|false
8739 */
8740 private function get_themes_data_for_api() {
8741 // Alias.
8742 $option_name = 'all_themes';
8743
8744 $all_cached_themes = self::$_accounts->get_option( $option_name );
8745
8746 if ( ! is_object( $all_cached_themes ) ) {
8747 $all_cached_themes = (object) array(
8748 'timestamp' => '',
8749 'md5' => '',
8750 'themes' => array(),
8751 );
8752 }
8753
8754 $time = time();
8755
8756 if ( ! empty( $all_cached_themes->timestamp ) &&
8757 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8758 ) {
8759 // Don't send theme updates if last update was in the past 5 min.
8760 return false;
8761 }
8762
8763 // Write timestamp to lock the logic.
8764 $all_cached_themes->timestamp = $time;
8765 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8766
8767 // Reload options from DB.
8768 self::$_accounts->load( true );
8769 $all_cached_themes = self::$_accounts->get_option( $option_name );
8770
8771 if ( $time != $all_cached_themes->timestamp ) {
8772 // If timestamp is different, then another thread captured the lock.
8773 return false;
8774 }
8775
8776 // Get active theme.
8777 $active_theme = wp_get_theme();
8778 $active_theme_stylesheet = $active_theme->get_stylesheet();
8779
8780 // Check if there's a change in themes.
8781 $all_themes = wp_get_themes();
8782
8783 // Check if themes changed.
8784 ksort( $all_themes );
8785
8786 $themes_signature = '';
8787 foreach ( $all_themes as $slug => $data ) {
8788 $is_active = ( $slug === $active_theme_stylesheet );
8789 $themes_signature .= $slug . ',' .
8790 $data->version . ',' .
8791 ( $is_active ? '1' : '0' ) . ';';
8792 }
8793
8794 // Check if themes status changed (version or active/inactive).
8795 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8796
8797 $themes_update_data = array();
8798
8799 if ( $themes_changed ) {
8800 // Change in themes, report changes.
8801
8802 // Update existing themes info.
8803 foreach ( $all_cached_themes->themes as $slug => $data ) {
8804 $is_active = ( $slug === $active_theme_stylesheet );
8805
8806 if ( ! isset( $all_themes[ $slug ] ) ) {
8807 // Plugin uninstalled.
8808 $uninstalled_theme_data = $data;
8809 $uninstalled_theme_data['is_active'] = false;
8810 $uninstalled_theme_data['is_uninstalled'] = true;
8811 $themes_update_data[] = $uninstalled_theme_data;
8812
8813 unset( $all_themes[ $slug ] );
8814 unset( $all_cached_themes->themes[ $slug ] );
8815 } else if ( $data['is_active'] !== $is_active ||
8816 $data['version'] !== $all_themes[ $slug ]->version
8817 ) {
8818 // Plugin activated or deactivated, or version changed.
8819
8820 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
8821 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
8822
8823 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
8824 }
8825 }
8826
8827 // Find new themes that weren't yet seen before.
8828 foreach ( $all_themes as $slug => $data ) {
8829 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
8830 $is_active = ( $slug === $active_theme_stylesheet );
8831
8832 // New plugin.
8833 $new_plugin = array(
8834 'slug' => $slug,
8835 'version' => $data->version,
8836 'title' => $data->name,
8837 'is_active' => $is_active,
8838 'is_uninstalled' => false,
8839 );
8840
8841 $themes_update_data[] = $new_plugin;
8842 $all_cached_themes->themes[ $slug ] = $new_plugin;
8843 }
8844 }
8845
8846 $all_cached_themes->md5 = md5( $themes_signature );
8847 $all_cached_themes->timestamp = time();
8848 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8849 }
8850
8851 return $themes_update_data;
8852 }
8853
8854 /**
8855 * Get site data for API install request.
8856 *
8857 * @author Vova Feldman (@svovaf)
8858 * @since 1.1.2
8859 *
8860 * @param string[] $override
8861 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
8862 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
8863 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, charset, title, and URL).
8864 *
8865 * @return array
8866 */
8867 private function get_install_data_for_api(
8868 array $override,
8869 $include_plugins = true,
8870 $include_themes = true,
8871 $include_blog_data = true
8872 ) {
8873 if ( $this->is_extensions_tracking_allowed() ) {
8874 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
8875 /**
8876 * @since 1.1.8 Also send plugin updates.
8877 */
8878 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
8879 $plugins = $this->get_plugins_data_for_api();
8880 if ( ! empty( $plugins ) ) {
8881 $override['plugins'] = $plugins;
8882 }
8883 }
8884 }
8885
8886 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
8887 /**
8888 * @since 1.1.8 Also send themes updates.
8889 */
8890 if ( $include_themes && ! isset( $override['themes'] ) ) {
8891 $themes = $this->get_themes_data_for_api();
8892 if ( ! empty( $themes ) ) {
8893 $override['themes'] = $themes;
8894 }
8895 }
8896 }
8897 }
8898
8899 $versions = $this->get_versions();
8900
8901 $blog_data = $include_blog_data ?
8902 array(
8903 'language' => get_bloginfo( 'language' ),
8904 'charset' => get_bloginfo( 'charset' ),
8905 'title' => get_bloginfo( 'name' ),
8906 'url' => get_site_url(),
8907 ) :
8908 array();
8909
8910 return array_merge( $versions, $blog_data, array(
8911 'version' => $this->get_plugin_version(),
8912 'is_premium' => $this->is_premium(),
8913 // Special params.
8914 'is_active' => true,
8915 'is_disconnected' => $this->is_tracking_prohibited(),
8916 'is_uninstalled' => false,
8917 ), $override );
8918 }
8919
8920 /**
8921 * Update installs details.
8922 *
8923 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
8924 *
8925 * @author Vova Feldman (@svovaf)
8926 * @since 2.0.0
8927 *
8928 * @param string[] string $override
8929 * @param bool $only_diff
8930 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
8931 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
8932 *
8933 * @return array
8934 */
8935 private function get_installs_data_for_api(
8936 array $override,
8937 $only_diff = false,
8938 $include_plugins = true,
8939 $include_themes = true
8940 ) {
8941 /**
8942 * @since 1.1.8 Also send plugin updates.
8943 */
8944 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
8945 // $plugins = $this->get_plugins_data_for_api();
8946 // if ( ! empty( $plugins ) ) {
8947 // $override['plugins'] = $plugins;
8948 // }
8949 // }
8950 /**
8951 * @since 1.1.8 Also send themes updates.
8952 */
8953 // if ( $include_themes && ! isset( $override['themes'] ) ) {
8954 // $themes = $this->get_themes_data_for_api();
8955 // if ( ! empty( $themes ) ) {
8956 // $override['themes'] = $themes;
8957 // }
8958 // }
8959
8960 // Common properties.
8961 $versions = $this->get_versions();
8962 $common = array_merge( $versions, array(
8963 'version' => $this->get_plugin_version(),
8964 'is_premium' => $this->is_premium(),
8965 ), $override );
8966
8967
8968 $is_common_diff_for_any_site = false;
8969 $common_diff_union = array();
8970
8971 $installs_data = array();
8972
8973 $sites = self::get_sites();
8974
8975 foreach ( $sites as $site ) {
8976 $blog_id = self::get_site_blog_id( $site );
8977
8978 $install = $this->get_install_by_blog_id( $blog_id );
8979
8980 if ( is_object( $install ) ) {
8981 if ( $install->user_id != $this->_user->id ) {
8982 // Install belongs to a different owner.
8983 continue;
8984 }
8985
8986 if ( ! $this->is_premium() && $install->is_tracking_prohibited() ) {
8987 // Don't send updates regarding opted-out installs.
8988 continue;
8989 }
8990
8991 $install_data = $this->get_site_info( $site );
8992
8993 $uid = $install_data['uid'];
8994
8995 unset( $install_data['blog_id'] );
8996 unset( $install_data['uid'] );
8997
8998 $install_data['is_disconnected'] = $install->is_disconnected;
8999 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9000 $install_data['is_uninstalled'] = $install->is_uninstalled;
9001
9002 $common_diff = null;
9003 $is_common_diff = false;
9004 if ( $only_diff ) {
9005 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9006 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9007
9008 $is_common_diff = ! empty( $common_diff );
9009
9010 if ( $is_common_diff ) {
9011 foreach ( $common_diff as $k => $v ) {
9012 if ( ! isset( $common_diff_union[ $k ] ) ) {
9013 $common_diff_union[ $k ] = $v;
9014 }
9015 }
9016 }
9017
9018 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9019 }
9020
9021 if ( ! empty( $install_data ) || $is_common_diff ) {
9022 // Add install ID and site unique ID.
9023 $install_data['id'] = $install->id;
9024 $install_data['uid'] = $uid;
9025
9026 $installs_data[] = $install_data;
9027 }
9028 }
9029 }
9030
9031 restore_current_blog();
9032
9033 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9034 if ( ! $only_diff ) {
9035 $installs_data[] = $common;
9036 } else if ( ! empty( $common_diff_union ) ) {
9037 $installs_data[] = $common_diff_union;
9038 }
9039 }
9040
9041 foreach ( $installs_data as &$data ) {
9042 $data = (object) $data;
9043 }
9044
9045 return $installs_data;
9046 }
9047
9048 /**
9049 * Compare site actual data to the stored install data and return the differences for an API data sync.
9050 *
9051 * @author Vova Feldman (@svovaf)
9052 * @since 2.0.0
9053 *
9054 * @param array $site
9055 * @param FS_Site $install
9056 * @param string[] string $override
9057 *
9058 * @return array
9059 */
9060 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9061 $diff = array();
9062 $special = array();
9063 $special_override = false;
9064
9065 foreach ( $site as $p => $v ) {
9066 if ( property_exists( $install, $p ) ) {
9067 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9068 $install->{$p} != $v
9069 ) {
9070 $install->{$p} = $v;
9071 $diff[ $p ] = $v;
9072 }
9073 } else {
9074 $special[ $p ] = $v;
9075
9076 if ( isset( $override[ $p ] ) ||
9077 'plugins' === $p ||
9078 'themes' === $p
9079 ) {
9080 $special_override = true;
9081 }
9082 }
9083 }
9084
9085 if ( $special_override || 0 < count( $diff ) ) {
9086 // Add special params only if has at least one
9087 // standard param, or if explicitly requested to
9088 // override a special param or a param which is not exist
9089 // in the install object.
9090 $diff = array_merge( $diff, $special );
9091 }
9092
9093 return $diff;
9094 }
9095
9096 /**
9097 * Update install only if changed.
9098 *
9099 * @author Vova Feldman (@svovaf)
9100 * @since 1.0.9
9101 *
9102 * @param string[] string $override
9103 * @param bool $flush
9104 *
9105 * @return false|object|string
9106 */
9107 private function send_install_update( $override = array(), $flush = false ) {
9108 $this->_logger->entrance();
9109
9110 $check_properties = $this->get_install_data_for_api( $override );
9111
9112 if ( $flush ) {
9113 $params = $check_properties;
9114 } else {
9115 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9116 }
9117
9118 $keepalive_only_update = false;
9119 if ( empty( $params ) ) {
9120 $keepalive_only_update = $this->should_send_keepalive_update();
9121
9122 if ( ! $keepalive_only_update ) {
9123 /**
9124 * There are no updates to send including keepalive.
9125 *
9126 * @author Leo Fajardo (@leorw)
9127 * @since 2.2.3
9128 */
9129 return false;
9130 }
9131 }
9132
9133 if ( ! $keepalive_only_update ) {
9134 /**
9135 * Do not update the last install sync timestamp after a keepalive-only call since there were no actual
9136 * updates sent.
9137 *
9138 * @author Leo Fajardo (@leorw)
9139 * @since 2.2.3
9140 */
9141 if ( ! is_multisite() ) {
9142 // Update last install sync timestamp.
9143 $this->set_cron_execution_timestamp( 'install_sync' );
9144 }
9145
9146 $params['uid'] = $this->get_anonymous_id();
9147 }
9148
9149 $this->set_keepalive_timestamp();
9150
9151 // Send updated values to FS.
9152 $site = $this->get_api_site_scope()->call( '/', 'put', $params );
9153
9154 if ( ! $keepalive_only_update && $this->is_api_result_entity( $site ) ) {
9155 /**
9156 * Do not clear scheduled sync after a keepalive-only call since there were no actual updates sent.
9157 *
9158 * @author Leo Fajardo (@leorw)
9159 * @since 2.2.3
9160 */
9161 if ( ! is_multisite() ) {
9162 // I successfully sent install update, clear scheduled sync if exist.
9163 $this->clear_install_sync_cron();
9164 }
9165 }
9166
9167 return $site;
9168 }
9169
9170 /**
9171 * Update installs only if changed.
9172 *
9173 * @author Vova Feldman (@svovaf)
9174 * @since 2.0.0
9175 *
9176 * @param string[] string $override
9177 * @param bool $flush
9178 *
9179 * @return false|object|string
9180 */
9181 private function send_installs_update( $override = array(), $flush = false ) {
9182 $this->_logger->entrance();
9183
9184 $installs_data = $this->get_installs_data_for_api( $override, ! $flush );
9185
9186 $keepalive_only_update = false;
9187 if ( empty( $installs_data ) ) {
9188 /**
9189 * Pass `true` to use the network level storage since the update is for many installs.
9190 *
9191 * @author Leo Fajardo (@leorw)
9192 * @since 2.2.3
9193 */
9194 $keepalive_only_update = $this->should_send_keepalive_update( true );
9195
9196 if ( ! $keepalive_only_update ) {
9197 /**
9198 * There are no updates to send including keepalive.
9199 *
9200 * @author Leo Fajardo (@leorw)
9201 * @since 2.2.3
9202 */
9203 return false;
9204 }
9205 }
9206
9207 if ( ! $keepalive_only_update ) {
9208 // Update last install sync timestamp if there were actual updates sent (i.e., not a keepalive-only call).
9209 $this->set_cron_execution_timestamp( 'install_sync' );
9210 }
9211
9212 /**
9213 * Pass `true` to use the network level storage since the update is for many installs.
9214 *
9215 * @author Leo Fajardo (@leorw)
9216 * @since 2.2.3
9217 */
9218 $this->set_keepalive_timestamp( true );
9219
9220 // Send updated values to FS.
9221 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9222
9223 if ( ! $keepalive_only_update && $this->is_api_result_object( $result, 'installs' ) ) {
9224 // I successfully sent installs update (there was an actual update sent and it's not just a keepalive-only call), clear scheduled sync if exist.
9225 $this->clear_install_sync_cron();
9226 }
9227
9228 return $result;
9229 }
9230
9231 /**
9232 * @author Leo Fajardo (@leorw)
9233 *
9234 * @param bool|null $use_network_level_storage
9235 *
9236 * @return bool
9237 */
9238 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9239 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9240
9241 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9242 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9243 return true;
9244 } else {
9245 // 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.
9246 return ( 7 == rand( 1, 7 ) );
9247 }
9248 }
9249
9250 /**
9251 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9252 *
9253 * @author Leo Fajardo (@leorw)
9254 * @since 2.3.2
9255 */
9256 private function maybe_sync_install_user() {
9257 if ( $this->_user->id == $this->_site->user_id ) {
9258 return;
9259 }
9260
9261 // Fetch user data and store if found.
9262 $this->sync_user_by_current_install();
9263 }
9264
9265 /**
9266 * Update install only if changed.
9267 *
9268 * @author Vova Feldman (@svovaf)
9269 * @since 1.0.9
9270 *
9271 * @param string[] string $override
9272 * @param bool $flush
9273 */
9274 private function sync_install( $override = array(), $flush = false ) {
9275 $this->_logger->entrance();
9276
9277 $site = $this->send_install_update( $override, $flush );
9278
9279 if ( false === $site ) {
9280 // No sync required.
9281 return;
9282 }
9283
9284 if ( ! $this->is_api_result_entity( $site ) ) {
9285 // Failed to sync, don't update locally.
9286 return;
9287 }
9288
9289 $this->_site = new FS_Site( $site );
9290
9291 $this->_store_site( true );
9292 }
9293
9294 /**
9295 * Update install only if changed.
9296 *
9297 * @author Vova Feldman (@svovaf)
9298 * @since 1.0.9
9299 *
9300 * @param string[] string $override
9301 * @param bool $flush
9302 */
9303 private function sync_installs( $override = array(), $flush = false ) {
9304 $this->_logger->entrance();
9305
9306 $result = $this->send_installs_update( $override, $flush );
9307
9308 if ( false === $result ) {
9309 // No sync required.
9310 return;
9311 }
9312
9313 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9314 // Failed to sync, don't update locally.
9315 return;
9316 }
9317
9318 $address_to_blog_map = $this->get_address_to_blog_map();
9319
9320 foreach ( $result->installs as $install ) {
9321 $this->_site = new FS_Site( $install );
9322
9323 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9324 $blog_id = $address_to_blog_map[ $address ];
9325
9326 $this->_store_site( true, $blog_id );
9327 }
9328 }
9329
9330 /**
9331 * Track install's custom event.
9332 *
9333 * IMPORTANT:
9334 * Custom event tracking is currently only supported for specific clients.
9335 * If you are not one of them, please don't use this method. If you will,
9336 * the API will simply ignore your request based on the plugin ID.
9337 *
9338 * Need custom tracking for your plugin or theme?
9339 * If you are interested in custom event tracking please contact yo@freemius.com
9340 * for further details.
9341 *
9342 * @author Vova Feldman (@svovaf)
9343 * @since 1.2.1
9344 *
9345 * @param string $name Event name.
9346 * @param array $properties Associative key/value array with primitive values only
9347 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9348 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9349 *
9350 * @return object|false Event data or FALSE on failure.
9351 *
9352 * @throws \Freemius_InvalidArgumentException
9353 */
9354 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9355 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9356
9357 if ( ! $this->is_registered() ) {
9358 return false;
9359 }
9360
9361 $event = array( 'type' => $name );
9362
9363 if ( is_numeric( $process_at ) && $process_at > time() ) {
9364 $event['process_at'] = $process_at;
9365 }
9366
9367 if ( $once ) {
9368 $event['once'] = true;
9369 }
9370
9371 if ( ! empty( $properties ) ) {
9372 // Verify associative array values are primitive.
9373 foreach ( $properties as $k => $v ) {
9374 if ( ! is_scalar( $v ) ) {
9375 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9376 }
9377 }
9378
9379 $event['properties'] = $properties;
9380 }
9381
9382 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9383
9384 return $this->is_api_error( $result ) ?
9385 false :
9386 $result;
9387 }
9388
9389 /**
9390 * Track install's custom event only once, but it still triggers the API call.
9391 *
9392 * IMPORTANT:
9393 * Custom event tracking is currently only supported for specific clients.
9394 * If you are not one of them, please don't use this method. If you will,
9395 * the API will simply ignore your request based on the plugin ID.
9396 *
9397 * Need custom tracking for your plugin or theme?
9398 * If you are interested in custom event tracking please contact yo@freemius.com
9399 * for further details.
9400 *
9401 * @author Vova Feldman (@svovaf)
9402 * @since 1.2.1
9403 *
9404 * @param string $name Event name.
9405 * @param array $properties Associative key/value array with primitive values only
9406 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9407 *
9408 * @return object|false Event data or FALSE on failure.
9409 *
9410 * @throws \Freemius_InvalidArgumentException
9411 *
9412 * @user Freemius::track_event()
9413 */
9414 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9415 return $this->track_event( $name, $properties, $process_at, true );
9416 }
9417
9418 /**
9419 * Plugin uninstall hook.
9420 *
9421 * @author Vova Feldman (@svovaf)
9422 * @since 1.0.1
9423 *
9424 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9425 */
9426 function _uninstall_plugin_event( $check_user = true ) {
9427 $this->_logger->entrance( 'slug = ' . $this->_slug );
9428
9429 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9430 return;
9431 }
9432
9433 $params = array();
9434 $uninstall_reason = null;
9435 if ( isset( $this->_storage->uninstall_reason ) ) {
9436 $uninstall_reason = $this->_storage->uninstall_reason;
9437 $params['reason_id'] = $uninstall_reason->id;
9438 $params['reason_info'] = $uninstall_reason->info;
9439 }
9440
9441 if ( ! $this->is_registered() ) {
9442 // Send anonymous uninstall event only if user submitted a feedback.
9443 if ( isset( $uninstall_reason ) ) {
9444 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9445 $this->opt_in( false, false, false, false, true );
9446 } else {
9447 $params['uid'] = $this->get_anonymous_id();
9448 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9449 }
9450 }
9451 } else {
9452 $params = array_merge( $params, array(
9453 'is_active' => false,
9454 'is_uninstalled' => true,
9455 ) );
9456
9457 if ( $this->_is_network_active ) {
9458 // Send uninstall event.
9459 $this->send_installs_update( $params );
9460 } else {
9461 // Send uninstall event.
9462 $this->send_install_update( $params );
9463 }
9464 }
9465
9466 // @todo Decide if we want to delete plugin information from db.
9467 }
9468
9469 /**
9470 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9471 *
9472 * @author Vova Feldman (@svovaf)
9473 * @since 2.2.1
9474 *
9475 * @param string $is_premium
9476 * @param string $caller
9477 *
9478 * @return string
9479 */
9480 function set_basename( $is_premium, $caller ) {
9481 $basename = plugin_basename( $caller );
9482
9483 $current_basename = $is_premium ?
9484 $this->_premium_plugin_basename :
9485 $this->_free_plugin_basename;
9486
9487 if ( $current_basename == $basename ) {
9488 // Basename value set correctly.
9489 return;
9490 }
9491
9492 if ( $is_premium ) {
9493 $this->_premium_plugin_basename = $basename;
9494 } else {
9495 $this->_free_plugin_basename = $basename;
9496 }
9497
9498 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9499
9500 register_activation_hook(
9501 $plugin_dir . $basename,
9502 array( &$this, '_activate_plugin_event_hook' )
9503 );
9504 }
9505
9506 /**
9507 * @author Vova Feldman (@svovaf)
9508 * @since 1.1.1
9509 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9510 *
9511 * @return string
9512 */
9513 function premium_plugin_basename() {
9514 if ( ! isset( $this->_premium_plugin_basename ) ) {
9515 $this->_premium_plugin_basename = $this->is_premium() ?
9516 // The product is premium, so use the current basename.
9517 $this->_plugin_basename :
9518 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9519 }
9520
9521 return $this->_premium_plugin_basename;
9522 }
9523
9524 /**
9525 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9526 *
9527 * @author Vova Feldman (@svovaf)
9528 * @since 1.0.2
9529 */
9530 public static function _uninstall_plugin_hook() {
9531 self::_load_required_static();
9532
9533 self::$_static_logger->entrance();
9534
9535 if ( ! current_user_can( 'activate_plugins' ) ) {
9536 return;
9537 }
9538
9539 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9540
9541 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9542
9543 define( 'WP_FS__UNINSTALL_MODE', true );
9544
9545 $fs = self::get_instance_by_file( $plugin_file );
9546
9547 if ( is_object( $fs ) ) {
9548 $fs->remove_sdk_reference();
9549
9550 self::require_plugin_essentials();
9551
9552 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9553 is_plugin_active( $fs->premium_plugin_basename() )
9554 ) {
9555 // Deleting Free or Premium plugin version while the other version still installed.
9556 return;
9557 }
9558
9559 $fs->_uninstall_plugin_event();
9560
9561 $fs->do_action( 'after_uninstall' );
9562 }
9563 }
9564
9565 #----------------------------------------------------------------------------------
9566 #region Plugin Information
9567 #----------------------------------------------------------------------------------
9568
9569 /**
9570 * Load WordPress core plugin.php essential module.
9571 *
9572 * @author Vova Feldman (@svovaf)
9573 * @since 1.1.1
9574 */
9575 private static function require_plugin_essentials() {
9576 if ( ! function_exists( 'get_plugins' ) ) {
9577 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9578
9579 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9580 }
9581 }
9582
9583 /**
9584 * Load WordPress core pluggable.php module.
9585 *
9586 * @author Vova Feldman (@svovaf)
9587 * @since 1.1.2
9588 */
9589 private static function require_pluggable_essentials() {
9590 if ( ! function_exists( 'wp_get_current_user' ) ) {
9591 require_once ABSPATH . 'wp-includes/pluggable.php';
9592 }
9593 }
9594
9595 /**
9596 * Return plugin data.
9597 *
9598 * @author Vova Feldman (@svovaf)
9599 * @since 1.0.1
9600 *
9601 * @param bool $reparse_plugin_metadata
9602 *
9603 * @return array
9604 */
9605 function get_plugin_data( $reparse_plugin_metadata = false ) {
9606 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9607 self::require_plugin_essentials();
9608
9609 if ( $this->is_plugin() ) {
9610 /**
9611 * @author Vova Feldman (@svovaf)
9612 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9613 *
9614 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9615 */
9616 $plugin_data = get_plugin_data(
9617 $this->_plugin_main_file_path,
9618 false,
9619 false
9620 );
9621 } else {
9622 $theme_data = wp_get_theme();
9623
9624 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9625 $parent_theme = $theme_data->parent();
9626
9627 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9628 $theme_data = $parent_theme;
9629 }
9630 }
9631
9632 $plugin_data = array(
9633 'Name' => $theme_data->get( 'Name' ),
9634 'Version' => $theme_data->get( 'Version' ),
9635 'Author' => $theme_data->get( 'Author' ),
9636 'Description' => $theme_data->get( 'Description' ),
9637 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9638 );
9639 }
9640
9641 $this->_plugin_data = $plugin_data;
9642 }
9643
9644 return $this->_plugin_data;
9645 }
9646
9647 /**
9648 * @author Vova Feldman (@svovaf)
9649 * @since 1.0.1
9650 * @since 1.2.2.5 If slug not set load slug by module ID.
9651 *
9652 * @return string Plugin slug.
9653 */
9654 function get_slug() {
9655 if ( ! isset( $this->_slug ) ) {
9656 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9657 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9658 }
9659
9660 return $this->_slug;
9661 }
9662
9663 /**
9664 * @author Leo Fajardo (@leorw)
9665 * @since 2.2.1
9666 *
9667 * @return string
9668 */
9669 function get_premium_slug() {
9670 return is_object( $this->_plugin ) ?
9671 $this->_plugin->premium_slug :
9672 "{$this->_slug}-premium";
9673 }
9674
9675 /**
9676 * Retrieve the desired folder name for the product.
9677 *
9678 * @author Vova Feldman (@svovaf)
9679 * @since 1.2.1.7
9680 *
9681 * @return string Plugin slug.
9682 */
9683 function get_target_folder_name() {
9684 return $this->can_use_premium_code() ?
9685 $this->_plugin->premium_slug :
9686 $this->_slug;
9687 }
9688
9689 /**
9690 * @author Vova Feldman (@svovaf)
9691 * @since 1.0.1
9692 *
9693 * @return number Plugin ID.
9694 */
9695 function get_id() {
9696 return $this->_plugin->id;
9697 }
9698
9699 /**
9700 * @author Leo Fajardo (@leorw)
9701 * @since 2.2.4
9702 *
9703 * @return number|null Bundle ID.
9704 */
9705 function get_bundle_id() {
9706 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
9707 $this->_plugin->bundle_id :
9708 null;
9709 }
9710
9711 /**
9712 * @author Vova Feldman (@svovaf)
9713 * @since 2.3.1
9714 *
9715 * @return string|null Bundle public key.
9716 */
9717 function get_bundle_public_key() {
9718 return isset( $this->_plugin->bundle_public_key ) ?
9719 $this->_plugin->bundle_public_key :
9720 null;
9721 }
9722
9723 /**
9724 * @author Vova Feldman (@svovaf)
9725 * @since 1.2.1.5
9726 *
9727 * @return string Freemius SDK version
9728 */
9729 function get_sdk_version() {
9730 return $this->version;
9731 }
9732
9733 /**
9734 * @author Vova Feldman (@svovaf)
9735 * @since 1.2.1.5
9736 *
9737 * @return number Parent plugin ID (if parent exist).
9738 */
9739 function get_parent_id() {
9740 return $this->is_addon() ?
9741 $this->get_parent_instance()->get_id() :
9742 $this->_plugin->id;
9743 }
9744
9745 /**
9746 * @author Vova Feldman (@svovaf)
9747 * @since 2.3.1
9748 *
9749 * @return string
9750 */
9751 function get_usage_tracking_terms_url() {
9752 return $this->apply_filters(
9753 'usage_tracking_terms_url',
9754 "https://freemius.com/wordpress/usage-tracking/{$this->_plugin->id}/{$this->_slug}/"
9755 );
9756 }
9757
9758 /**
9759 * @author Vova Feldman (@svovaf)
9760 * @since 2.3.1
9761 *
9762 * @return string
9763 */
9764 function get_eula_url() {
9765 return $this->apply_filters(
9766 'eula_url',
9767 "https://freemius.com/terms/{$this->_plugin->id}/{$this->_slug}/"
9768 );
9769 }
9770
9771 /**
9772 * @author Vova Feldman (@svovaf)
9773 * @since 1.0.1
9774 *
9775 * @return string Plugin public key.
9776 */
9777 function get_public_key() {
9778 return $this->_plugin->public_key;
9779 }
9780
9781 /**
9782 * Will be available only on sandbox mode.
9783 *
9784 * @author Vova Feldman (@svovaf)
9785 * @since 1.0.4
9786 *
9787 * @return mixed Plugin secret key.
9788 */
9789 function get_secret_key() {
9790 return $this->_plugin->secret_key;
9791 }
9792
9793 /**
9794 * @author Vova Feldman (@svovaf)
9795 * @since 1.1.1
9796 *
9797 * @return bool
9798 */
9799 function has_secret_key() {
9800 return ! empty( $this->_plugin->secret_key );
9801 }
9802
9803 /**
9804 * @author Vova Feldman (@svovaf)
9805 * @since 1.0.9
9806 *
9807 * @param string|bool $premium_suffix
9808 *
9809 * @return string
9810 */
9811 function get_plugin_name( $premium_suffix = false ) {
9812 $this->_logger->entrance();
9813
9814 /**
9815 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
9816 *
9817 * @author Vova Feldman
9818 */
9819 if ( ! isset( $this->_plugin_name ) ) {
9820 // Name is not yet set.
9821 $this->set_name( $premium_suffix );
9822 } else if (
9823 ! empty( $premium_suffix ) &&
9824 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
9825 ) {
9826 // Name is already set, but there's a change in the premium suffix.
9827 $this->set_name( $premium_suffix );
9828 }
9829
9830 return $this->_plugin_name;
9831 }
9832
9833 /**
9834 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
9835 *
9836 * @author Vova Feldman (@svovaf)
9837 * @since 2.2.1
9838 *
9839 * @param string $premium_suffix
9840 */
9841 private function set_name( $premium_suffix = '' ) {
9842 $plugin_data = $this->get_plugin_data();
9843
9844 // Get name.
9845 $this->_plugin_name = $plugin_data['Name'];
9846
9847 if ( is_string( $premium_suffix ) ) {
9848 $premium_suffix = trim( $premium_suffix );
9849
9850 if ( ! empty( $premium_suffix ) ) {
9851 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
9852 $suffix = ( ' ' . strtolower( $premium_suffix ) );
9853 $suffix_len = strlen( $suffix );
9854
9855 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
9856 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
9857 ) {
9858 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
9859 }
9860 }
9861 }
9862
9863 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
9864 }
9865
9866 /**
9867 * @author Vova Feldman (@svovaf)
9868 * @since 1.0.0
9869 *
9870 * @param bool $reparse_plugin_metadata
9871 *
9872 * @return string
9873 */
9874 function get_plugin_version( $reparse_plugin_metadata = false ) {
9875 $this->_logger->entrance();
9876
9877 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
9878
9879 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
9880
9881 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
9882 }
9883
9884 /**
9885 * @author Vova Feldman (@svovaf)
9886 * @since 1.2.1.7
9887 *
9888 * @return string
9889 */
9890 function get_plugin_title() {
9891 $this->_logger->entrance();
9892
9893 $title = $this->_plugin->title;
9894
9895 return $this->apply_filters( 'plugin_title', $title );
9896 }
9897
9898 /**
9899 * @author Vova Feldman (@svovaf)
9900 * @since 1.2.2.7
9901 *
9902 * @param bool $lowercase
9903 *
9904 * @return string
9905 */
9906 function get_module_label( $lowercase = false ) {
9907 $label = $this->is_addon() ?
9908 $this->get_text_inline( 'Add-On', 'addon' ) :
9909 ( $this->is_plugin() ?
9910 $this->get_text_inline( 'Plugin', 'plugin' ) :
9911 $this->get_text_inline( 'Theme', 'theme' ) );
9912
9913 if ( $lowercase ) {
9914 $label = strtolower( $label );
9915 }
9916
9917 return $label;
9918 }
9919
9920 /**
9921 * @author Vova Feldman (@svovaf)
9922 * @since 1.0.4
9923 *
9924 * @return string
9925 */
9926 function get_plugin_basename() {
9927 if ( ! isset( $this->_plugin_basename ) ) {
9928 if ( $this->is_plugin() ) {
9929 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
9930 } else {
9931 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
9932 }
9933 }
9934
9935 return $this->_plugin_basename;
9936 }
9937
9938 function get_plugin_folder_name() {
9939 $this->_logger->entrance();
9940
9941 $plugin_folder = $this->_plugin_basename;
9942
9943 while ( '.' !== dirname( $plugin_folder ) ) {
9944 $plugin_folder = dirname( $plugin_folder );
9945 }
9946
9947 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
9948
9949 return $plugin_folder;
9950 }
9951
9952 #endregion ------------------------------------------------------------------
9953
9954 /* Account
9955 ------------------------------------------------------------------------------------------------------------------*/
9956
9957 /**
9958 * Find plugin's slug by plugin's basename.
9959 *
9960 * @author Vova Feldman (@svovaf)
9961 * @since 1.0.9
9962 *
9963 * @param string $plugin_base_name
9964 *
9965 * @return false|string
9966 */
9967 private static function find_slug_by_basename( $plugin_base_name ) {
9968 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
9969
9970 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
9971 return false;
9972 }
9973
9974 return $file_slug_map[ $plugin_base_name ];
9975 }
9976
9977 /**
9978 * Store the map between the plugin's basename to the slug.
9979 *
9980 * @author Vova Feldman (@svovaf)
9981 * @since 1.0.9
9982 */
9983 private function store_file_slug_map() {
9984 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
9985
9986 if ( ! array( $file_slug_map ) ) {
9987 $file_slug_map = array();
9988 }
9989
9990 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
9991 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
9992 ) {
9993 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
9994 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
9995 }
9996 }
9997
9998 /**
9999 * @return array[number]FS_User
10000 */
10001 static function get_all_users() {
10002 $users = self::maybe_get_entities_account_option( 'users', array() );
10003
10004 if ( ! is_array( $users ) ) {
10005 $users = array();
10006 }
10007
10008 return $users;
10009 }
10010
10011 /**
10012 * @param string $module_type
10013 * @param null|int $blog_id Since 2.0.0
10014 *
10015 * @return array[string]FS_Site
10016 */
10017 private static function get_all_sites(
10018 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10019 $blog_id = null
10020 ) {
10021 $sites = self::get_account_option( 'sites', $module_type, $blog_id );
10022
10023 if ( ! is_array( $sites ) ) {
10024 $sites = array();
10025 }
10026
10027 return $sites;
10028 }
10029
10030 /**
10031 * @author Leo Fajardo (@leorw)
10032 *
10033 * @since 1.2.2
10034 *
10035 * @param string $option_name
10036 * @param string $module_type
10037 * @param null|int $network_level_or_blog_id Since 2.0.0
10038 *
10039 * @return mixed
10040 */
10041 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10042 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10043 $option_name = $module_type . '_' . $option_name;
10044 }
10045
10046 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10047 }
10048
10049 /**
10050 * @author Leo Fajardo (@leorw)
10051 *
10052 * @since 1.2.2
10053 *
10054 * @param string $option_name
10055 * @param mixed $option_value
10056 * @param bool $store
10057 * @param null|int $network_level_or_blog_id Since 2.0.0
10058 */
10059 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10060 self::set_account_option_by_module(
10061 $this->_module_type,
10062 $option_name,
10063 $option_value,
10064 $store,
10065 $network_level_or_blog_id
10066 );
10067 }
10068
10069 /**
10070 * @author Vova Feldman (@svovaf)
10071 *
10072 * @since 1.2.2.7
10073 *
10074 * @param string $module_type
10075 * @param string $option_name
10076 * @param mixed $option_value
10077 * @param bool $store
10078 * @param null|int $network_level_or_blog_id Since 2.0.0
10079 */
10080 private static function set_account_option_by_module(
10081 $module_type,
10082 $option_name,
10083 $option_value,
10084 $store,
10085 $network_level_or_blog_id = null
10086 ) {
10087 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10088 $option_name = $module_type . '_' . $option_name;
10089 }
10090
10091 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10092 }
10093
10094 /**
10095 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10096 *
10097 * @author Leo Fajardo (@leorw)
10098 * @since 2.3.1
10099 *
10100 * @param string $option_name
10101 * @param mixed $default
10102 * @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).
10103 *
10104 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10105 */
10106 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10107 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10108
10109 $class_name = '';
10110
10111 switch ( $option_name ) {
10112 case 'plugins':
10113 case 'themes':
10114 case 'addons':
10115 $class_name = FS_Plugin::get_class_name();
10116 break;
10117 case 'users':
10118 $class_name = FS_User::get_class_name();
10119 break;
10120 case 'sites':
10121 $class_name = FS_Site::get_class_name();
10122 break;
10123 case 'licenses':
10124 case 'all_licenses':
10125 $class_name = FS_Plugin_License::get_class_name();
10126 break;
10127 case 'plans':
10128 $class_name = FS_Plugin_Plan::get_class_name();
10129 break;
10130 case 'updates':
10131 $class_name = FS_Plugin_Tag::get_class_name();
10132 break;
10133 }
10134
10135 if ( empty( $class_name ) ) {
10136 return $option;
10137 }
10138
10139 return fs_get_entities( $option, $class_name );
10140 }
10141
10142 /**
10143 * @author Vova Feldman (@svovaf)
10144 * @since 1.0.6
10145 *
10146 * @param number|null $module_id
10147 *
10148 * @return FS_Plugin_License[]
10149 */
10150 private static function get_all_licenses( $module_id = null ) {
10151 $licenses = self::get_account_option( 'all_licenses' );
10152
10153 if ( ! is_array( $licenses ) ) {
10154 $licenses = array();
10155 }
10156
10157 if ( is_null( $module_id ) ) {
10158 return $licenses;
10159 }
10160
10161 $licenses = isset( $licenses[ $module_id ] ) ?
10162 $licenses[ $module_id ] :
10163 array();
10164
10165 return $licenses;
10166 }
10167
10168 /**
10169 * @author Leo Fajardo (@leorw)
10170 * @since 2.0.0
10171 *
10172 * @return array
10173 */
10174 private static function get_all_licenses_by_module_type() {
10175 $licenses = self::get_account_option( 'all_licenses' );
10176
10177 $licenses_by_module_type = array(
10178 WP_FS__MODULE_TYPE_PLUGIN => array(),
10179 WP_FS__MODULE_TYPE_THEME => array()
10180 );
10181
10182 if ( ! is_array( $licenses ) ) {
10183 return $licenses_by_module_type;
10184 }
10185
10186 foreach ( $licenses as $module_id => $module_licenses ) {
10187 $fs = self::get_instance_by_id( $module_id );
10188 if ( false === $fs ) {
10189 continue;
10190 }
10191
10192 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
10193 }
10194
10195 return $licenses_by_module_type;
10196 }
10197
10198 /**
10199 * @author Leo Fajardo (@leorw)
10200 * @since 2.0.0
10201 *
10202 * @param number $module_id
10203 * @param number|null $user_id
10204 *
10205 * @return array
10206 */
10207 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10208 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10209
10210 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10211 $all_modules_user_id_license_ids_map = array();
10212 }
10213
10214 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10215 $all_modules_user_id_license_ids_map[ $module_id ] :
10216 array();
10217
10218 if ( FS_User::is_valid_id( $user_id ) ) {
10219 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10220 $user_id_license_ids_map[ $user_id ] :
10221 array();
10222 }
10223
10224 return $user_id_license_ids_map;
10225 }
10226
10227 /**
10228 * @author Leo Fajardo (@leorw)
10229 * @since 2.0.0
10230 *
10231 * @param array $new_user_id_license_ids_map
10232 * @param number $module_id
10233 * @param number|null $user_id
10234 */
10235 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10236 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10237 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10238 $all_modules_user_id_license_ids_map = array();
10239 }
10240
10241 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10242 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10243 }
10244
10245 if ( FS_User::is_valid_id( $user_id ) ) {
10246 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10247 } else {
10248 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10249 }
10250
10251 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10252 }
10253
10254 /**
10255 * Get a collection of the user's linked license IDs.
10256 *
10257 * @author Vova Feldman (@svovaf)
10258 * @since 2.0.0
10259 *
10260 * @param number $user_id
10261 *
10262 * @return number[]
10263 */
10264 private function get_user_linked_license_ids( $user_id ) {
10265 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10266 }
10267
10268 /**
10269 * Override the user's linked license IDs with a new IDs collection.
10270 *
10271 * @author Vova Feldman (@svovaf)
10272 * @since 2.0.0
10273 *
10274 * @param number $user_id
10275 * @param number[] $license_ids
10276 */
10277 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10278 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10279 }
10280
10281 /**
10282 * Link a specified license ID to a given user.
10283 *
10284 * @author Vova Feldman (@svovaf)
10285 * @since 2.0.0
10286 *
10287 * @param number $license_id
10288 * @param number $user_id
10289 */
10290 private function link_license_2_user( $license_id, $user_id ) {
10291 $license_ids = $this->get_user_linked_license_ids( $user_id );
10292
10293 if ( in_array( $license_id, $license_ids ) ) {
10294 // License already linked.
10295 return;
10296 }
10297
10298 $license_ids[] = $license_id;
10299
10300 $this->set_user_linked_license_ids( $user_id, $license_ids );
10301 }
10302
10303 /**
10304 * @param string|bool $module_type
10305 *
10306 * @return FS_Plugin_Plan[]
10307 */
10308 private static function get_all_plans( $module_type = false ) {
10309 $plans = self::get_account_option( 'plans', $module_type );
10310
10311 if ( ! is_array( $plans ) ) {
10312 $plans = array();
10313 }
10314
10315 return $plans;
10316 }
10317
10318 /**
10319 * @author Vova Feldman (@svovaf)
10320 * @since 1.0.4
10321 *
10322 * @return FS_Plugin_Tag[]
10323 */
10324 private static function get_all_updates() {
10325 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10326
10327 if ( ! is_array( $updates ) ) {
10328 $updates = array();
10329 }
10330
10331 return $updates;
10332 }
10333
10334 /**
10335 * @author Vova Feldman (@svovaf)
10336 * @since 1.0.6
10337 *
10338 * @return array<number,FS_Plugin[]>|false
10339 */
10340 private static function get_all_addons() {
10341 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10342
10343 if ( ! is_array( $addons ) ) {
10344 $addons = array();
10345 }
10346
10347 return $addons;
10348 }
10349
10350 /**
10351 * @author Vova Feldman (@svovaf)
10352 * @since 1.0.6
10353 *
10354 * @return number[]|false
10355 */
10356 private static function get_all_account_addons() {
10357 $addons = self::$_accounts->get_option( 'account_addons', array() );
10358
10359 if ( ! is_array( $addons ) ) {
10360 $addons = array();
10361 }
10362
10363 return $addons;
10364 }
10365
10366 /**
10367 * Check if user has connected his account (opted-in).
10368 *
10369 * Note:
10370 * If the user opted-in and opted-out on a later stage,
10371 * this will still return true. If you want to check if the
10372 * user is currently opted-in, use:
10373 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10374 *
10375 * @author Vova Feldman (@svovaf)
10376 * @since 1.0.1
10377 * @return bool
10378 */
10379 function is_registered() {
10380 return is_object( $this->_user );
10381 }
10382
10383 /**
10384 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10385 *
10386 * @author Leo Fajardo (@leorw)
10387 * @since 1.2.1.5
10388 *
10389 * @return bool
10390 */
10391 function is_tracking_allowed() {
10392 return ( is_object( $this->_site ) && $this->_site->is_tracking_allowed() );
10393 }
10394
10395 /**
10396 * @author Vova Feldman (@svovaf)
10397 * @since 1.0.4
10398 *
10399 * @return FS_Plugin
10400 */
10401 function get_plugin() {
10402 return $this->_plugin;
10403 }
10404
10405 /**
10406 * @author Vova Feldman (@svovaf)
10407 * @since 1.0.3
10408 *
10409 * @return FS_User
10410 */
10411 function get_user() {
10412 return $this->_user;
10413 }
10414
10415 /**
10416 * @author Vova Feldman (@svovaf)
10417 * @since 1.0.3
10418 *
10419 * @return FS_Site
10420 */
10421 function get_site() {
10422 return $this->_site;
10423 }
10424
10425 /**
10426 * Get plugin add-ons.
10427 *
10428 * @author Vova Feldman (@svovaf)
10429 * @since 1.0.6
10430 *
10431 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10432 *
10433 * @param bool $flush
10434 *
10435 * @return FS_Plugin[]|false
10436 */
10437 function get_addons( $flush = false ) {
10438 $this->_logger->entrance();
10439
10440 if ( ! $this->_has_addons ) {
10441 return false;
10442 }
10443
10444 $addons = $this->sync_addons( $flush );
10445
10446 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10447 false :
10448 $addons;
10449 }
10450
10451 /**
10452 * @author Vova Feldman (@svovaf)
10453 * @since 1.0.6
10454 *
10455 * @return number[]|false
10456 */
10457 function get_account_addons() {
10458 $this->_logger->entrance();
10459
10460 $addons = self::get_all_account_addons();
10461
10462 if ( ! is_array( $addons ) ||
10463 ! isset( $addons[ $this->_plugin->id ] ) ||
10464 ! is_array( $addons[ $this->_plugin->id ] ) ||
10465 0 === count( $addons[ $this->_plugin->id ] )
10466 ) {
10467 return false;
10468 }
10469
10470 return $addons[ $this->_plugin->id ];
10471 }
10472
10473 /**
10474 * Check if user has any
10475 *
10476 * @author Vova Feldman (@svovaf)
10477 * @since 1.1.6
10478 *
10479 * @return bool
10480 */
10481 function has_account_addons() {
10482 $addons = $this->get_account_addons();
10483
10484 return is_array( $addons ) && ( 0 < count( $addons ) );
10485 }
10486
10487
10488 /**
10489 * Get add-on by ID (from local data).
10490 *
10491 * @author Vova Feldman (@svovaf)
10492 * @since 1.0.6
10493 *
10494 * @param number $id
10495 *
10496 * @return FS_Plugin|false
10497 */
10498 function get_addon( $id ) {
10499 $this->_logger->entrance();
10500
10501 $addons = $this->get_addons();
10502
10503 if ( is_array( $addons ) ) {
10504 foreach ( $addons as $addon ) {
10505 if ( $id == $addon->id ) {
10506 return $addon;
10507 }
10508 }
10509 }
10510
10511 return false;
10512 }
10513
10514 /**
10515 * Get add-on by slug (from local data).
10516 *
10517 * @author Vova Feldman (@svovaf)
10518 * @since 1.0.6
10519 *
10520 * @param string $slug
10521 *
10522 * @param bool $flush
10523 *
10524 * @return FS_Plugin|false
10525 */
10526 function get_addon_by_slug( $slug, $flush = false ) {
10527 $this->_logger->entrance();
10528
10529 $addons = $this->get_addons( $flush );
10530
10531 if ( is_array( $addons ) ) {
10532 foreach ( $addons as $addon ) {
10533 if ( $slug === $addon->slug ) {
10534 return $addon;
10535 }
10536 }
10537 }
10538
10539 return false;
10540 }
10541
10542 /**
10543 * @var array<number,object[]> {
10544 * @key number Add-on ID.
10545 * @val object[] The add-on's plans and prices object.
10546 * }
10547 */
10548 private $plans_and_pricing_by_addon_id;
10549
10550 /**
10551 * @author Leo Fajardo (@leorw)
10552 * @since 2.3.0
10553 *
10554 * @return array<number,object[]> {
10555 * @key number Add-on ID.
10556 * @val object[] The add-on's plans and prices object.
10557 * }
10558 */
10559 function _get_addons_plans_and_pricing_map_by_id() {
10560 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
10561 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
10562
10563 $plans_and_pricing_by_addon_id = array();
10564 if ( $this->is_api_result_object( $result, 'addons' ) ) {
10565 foreach ( $result->addons as $addon ) {
10566 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
10567 }
10568 }
10569
10570 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
10571 }
10572
10573 return $this->plans_and_pricing_by_addon_id;
10574 }
10575
10576 /**
10577 * @author Leo Fajardo (@leorw)
10578 * @since 2.3.0
10579 *
10580 * @param number $addon_id
10581 * @param bool $is_installed
10582 *
10583 * @return array
10584 */
10585 function _get_addon_info( $addon_id, $is_installed ) {
10586 $addon = $this->get_addon( $addon_id );
10587
10588 if ( ! is_object( $addon ) ) {
10589 // Unexpected call.
10590 return array();
10591 }
10592
10593 $slug = $addon->slug;
10594
10595 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
10596
10597 if ( ! fs_is_network_admin() ) {
10598 // Get blog-level activated installations.
10599 $sites = self::maybe_get_entities_account_option( 'sites', array() );
10600 } else {
10601 $sites = null;
10602
10603 if ( $this->is_addon_activated( $addon_id ) &&
10604 $this->get_addon_instance( $addon_id )->is_network_active()
10605 ) {
10606 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
10607 // Get network-level activated installations.
10608 $sites = self::maybe_get_entities_account_option(
10609 'sites',
10610 array(),
10611 $addon_storage->network_install_blog_id
10612 );
10613 }
10614 }
10615 }
10616
10617 $addon_info = array(
10618 'is_connected' => false,
10619 'slug' => $slug,
10620 'title' => $addon->title,
10621 'is_whitelabeled' => $addon_storage->is_whitelabeled
10622 );
10623
10624 if ( ! $is_installed ) {
10625 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
10626
10627 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
10628 $has_paid_plan = false;
10629 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
10630
10631 if ( is_array( $plans ) && count( $plans ) > 0 ) {
10632 foreach ( $plans as $plan ) {
10633 if ( isset( $plan->pricing ) &&
10634 is_array( $plan->pricing ) &&
10635 count( $plan->pricing ) > 0
10636 ) {
10637 $has_paid_plan = true;
10638 break;
10639 }
10640 }
10641 }
10642
10643 $addon_info['has_paid_plan'] = $has_paid_plan;
10644 }
10645 }
10646
10647 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
10648 return $addon_info;
10649 }
10650
10651 $site = $sites[ $slug ];
10652
10653 $addon_info['is_connected'] = (
10654 ( $addon->parent_plugin_id == $this->get_id() ) &&
10655 is_object( $site ) &&
10656 FS_Site::is_valid_id( $site->id ) &&
10657 FS_User::is_valid_id( $site->user_id ) &&
10658 FS_Plugin_Plan::is_valid_id( $site->plan_id )
10659 );
10660
10661 if ( $addon_info['is_connected'] && $is_installed ) {
10662 return $addon_info;
10663 }
10664
10665 $addon_info['site'] = $site;
10666
10667 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
10668 if ( isset( $plugins_data[ $slug ] ) ) {
10669 $plugin_data = $plugins_data[ $slug ];
10670
10671 $addon_info['version'] = $plugin_data->version;
10672 }
10673
10674 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
10675 if ( isset( $all_plans[ $slug ] ) ) {
10676 $plans = $all_plans[ $slug ];
10677
10678 foreach ( $plans as $plan ) {
10679 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
10680 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
10681 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
10682 break;
10683 }
10684 }
10685 }
10686
10687 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
10688 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
10689 foreach ( $licenses[ $addon_id ] as $license ) {
10690 if ( $license->id == $site->license_id ) {
10691 $addon_info['license'] = $license;
10692 break;
10693 }
10694 }
10695 }
10696
10697 if ( isset( $addon_info['license'] ) ) {
10698 if ( isset( $addon_storage->subscriptions ) &&
10699 ! empty( $addon_storage->subscriptions )
10700 ) {
10701 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
10702
10703 foreach ( $addon_subscriptions as $subscription ) {
10704 if ( $subscription->license_id == $site->license_id ) {
10705 $addon_info['subscription'] = $subscription;
10706 break;
10707 }
10708 }
10709 }
10710 }
10711
10712 return $addon_info;
10713 }
10714
10715 /**
10716 * @author Vova Feldman (@svovaf)
10717 * @since 2.0.0
10718 *
10719 * @param number $user_id
10720 *
10721 * @return FS_User
10722 */
10723 static function _get_user_by_id( $user_id ) {
10724 self::$_static_logger->entrance( "user_id = {$user_id}" );
10725
10726 $users = self::get_all_users();
10727
10728 if ( is_array( $users ) ) {
10729 if ( isset( $users[ $user_id ] ) &&
10730 $users[ $user_id ] instanceof FS_User &&
10731 $user_id == $users[ $user_id ]->id
10732 ) {
10733 return $users[ $user_id ];
10734 }
10735
10736 // If user wasn't found by the key, iterate over all the users collection.
10737 foreach ( $users as $user ) {
10738 /**
10739 * @var FS_User $user
10740 */
10741 if ( $user_id == $user->id ) {
10742 return $user;
10743 }
10744 }
10745 }
10746
10747 return null;
10748 }
10749
10750 /**
10751 * Checks if a Freemius user_id is associated with a super-admin.
10752 *
10753 * @author Vova Feldman (@svovaf)
10754 * @since 2.0.0
10755 *
10756 * @param number $user_id
10757 *
10758 * @return bool
10759 */
10760 private static function is_super_admin( $user_id ) {
10761 $is_super_admin = false;
10762
10763 $user = self::_get_user_by_id( $user_id );
10764
10765 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
10766 self::require_pluggable_essentials();
10767
10768 $wp_user = get_user_by( 'email', $user->email );
10769
10770 if ( $wp_user instanceof WP_User ) {
10771 $super_admins = get_super_admins();
10772 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
10773 }
10774 }
10775
10776 return $is_super_admin;
10777 }
10778
10779 #----------------------------------------------------------------------------------
10780 #region Plans & Licensing
10781 #----------------------------------------------------------------------------------
10782
10783 /**
10784 * Check if running premium plugin code.
10785 *
10786 * @author Vova Feldman (@svovaf)
10787 * @since 1.0.5
10788 *
10789 * @return bool
10790 */
10791 function is_premium() {
10792 /**
10793 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
10794 * `register_constructor_hooks` method.
10795 *
10796 * @author Leo Fajardo (@leorw)
10797 * @since 2.2.3
10798 */
10799 return is_object( $this->_plugin ) ?
10800 $this->_plugin->is_premium :
10801 false;
10802 }
10803
10804 /**
10805 * Get site's plan ID.
10806 *
10807 * @author Vova Feldman (@svovaf)
10808 * @since 1.0.2
10809 *
10810 * @return number
10811 */
10812 function get_plan_id() {
10813 return $this->_site->plan_id;
10814 }
10815
10816 /**
10817 * Get site's plan title.
10818 *
10819 * @author Vova Feldman (@svovaf)
10820 * @since 1.0.2
10821 *
10822 * @return string
10823 */
10824 function get_plan_title() {
10825 $plan = $this->get_plan();
10826
10827 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
10828 }
10829
10830 /**
10831 * Get site's plan name.
10832 *
10833 * @author Vova Feldman (@svovaf)
10834 * @since 2.0.0
10835 *
10836 * @return string
10837 */
10838 function get_plan_name() {
10839 $plan = $this->get_plan();
10840
10841 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
10842 }
10843
10844 /**
10845 * @author Vova Feldman (@svovaf)
10846 * @since 1.0.9
10847 *
10848 * @return FS_Plugin_Plan|false
10849 */
10850 function get_plan() {
10851 if ( ! is_object( $this->_site ) ) {
10852 return false;
10853 }
10854
10855 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
10856 $this->_get_plan_by_id( $this->_site->plan_id ) :
10857 false;
10858 }
10859
10860 /**
10861 * @author Vova Feldman (@svovaf)
10862 * @since 1.0.3
10863 *
10864 * @return bool
10865 */
10866 function is_trial() {
10867 $this->_logger->entrance();
10868
10869 if ( ! $this->is_registered() || ! is_object( $this->_site ) ) {
10870 return false;
10871 }
10872
10873 return $this->_site->is_trial();
10874 }
10875
10876 /**
10877 * Check if currently in a trial with payment method (credit card or paypal).
10878 *
10879 * @author Vova Feldman (@svovaf)
10880 * @since 1.1.7
10881 *
10882 * @return bool
10883 */
10884 function is_paid_trial() {
10885 $this->_logger->entrance();
10886
10887 if ( ! $this->is_trial() ) {
10888 return false;
10889 }
10890
10891 if ( ! $this->has_active_valid_license() ) {
10892 return false;
10893 }
10894
10895 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
10896 return false;
10897 }
10898
10899 /**
10900 * @var FS_Subscription $subscription
10901 */
10902 $subscription = $this->_get_subscription( $this->_license->id );
10903
10904 return ( is_object( $subscription ) && $subscription->is_active() );
10905 }
10906
10907 /**
10908 * Check if trial already utilized.
10909 *
10910 * @since 1.0.9
10911 *
10912 * @return bool
10913 */
10914 function is_trial_utilized() {
10915 $this->_logger->entrance();
10916
10917 if ( ! $this->is_registered() ) {
10918 return false;
10919 }
10920
10921 return $this->_site->is_trial_utilized();
10922 }
10923
10924 /**
10925 * Get trial plan information (if in trial).
10926 *
10927 * @author Vova Feldman (@svovaf)
10928 * @since 1.0.9
10929 *
10930 * @return bool|FS_Plugin_Plan
10931 */
10932 function get_trial_plan() {
10933 $this->_logger->entrance();
10934
10935 if ( ! $this->is_trial() ) {
10936 return false;
10937 }
10938
10939 // Try to load plan from local cache.
10940 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
10941
10942 if ( ! is_object( $trial_plan ) ) {
10943 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
10944
10945 /**
10946 * If managed to fetch the plan, add it to the plans collection.
10947 */
10948 if ( $trial_plan instanceof FS_Plugin_Plan ) {
10949 if ( ! is_array( $this->_plans ) ) {
10950 $this->_plans = array();
10951 }
10952
10953 $this->_plans[] = $trial_plan;
10954 $this->_store_plans();
10955 }
10956 }
10957
10958 if ( $trial_plan instanceof FS_Plugin_Plan ) {
10959 return $trial_plan;
10960 }
10961
10962 /**
10963 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
10964 */
10965 $trial_plan = new FS_Plugin_Plan();
10966 $trial_plan->id = $this->_site->trial_plan_id;
10967 $trial_plan->name = 'pro';
10968 $trial_plan->title = 'Pro';
10969
10970 return $trial_plan;
10971 }
10972
10973 /**
10974 * Check if the user has an activate, non-expired license on current plugin's install.
10975 *
10976 * @since 1.0.9
10977 *
10978 * @return bool
10979 */
10980 function is_paying() {
10981 $this->_logger->entrance();
10982
10983 if ( ! $this->is_registered() ) {
10984 return false;
10985 }
10986
10987 if ( ! $this->has_paid_plan() ) {
10988 return false;
10989 }
10990
10991 return (
10992 ! $this->is_trial() &&
10993 'free' !== $this->get_plan_name() &&
10994 $this->has_active_valid_license()
10995 );
10996 }
10997
10998 /**
10999 * @author Vova Feldman (@svovaf)
11000 * @since 1.0.4
11001 *
11002 * @return bool
11003 */
11004 function is_free_plan() {
11005 if ( ! $this->is_registered() ) {
11006 return true;
11007 }
11008
11009 if ( ! $this->has_paid_plan() ) {
11010 return true;
11011 }
11012
11013 return (
11014 'free' === $this->get_plan_name() ||
11015 ! $this->has_features_enabled_license()
11016 );
11017 }
11018
11019 /**
11020 * @author Vova Feldman (@svovaf)
11021 * @since 1.0.5
11022 *
11023 * @return bool
11024 */
11025 function _has_premium_license() {
11026 $this->_logger->entrance();
11027
11028 $premium_license = $this->_get_available_premium_license();
11029
11030 return ( false !== $premium_license );
11031 }
11032
11033 /**
11034 * Check if user has any licenses associated with the plugin (including expired or blocking).
11035 *
11036 * @author Vova Feldman (@svovaf)
11037 * @since 1.1.7.3
11038 *
11039 * @param bool $including_foreign
11040 *
11041 * @return bool
11042 */
11043 function has_any_license( $including_foreign = true ) {
11044 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11045 return false;
11046 }
11047
11048 if ( $including_foreign ) {
11049 return true;
11050 }
11051
11052 foreach ( $this->_licenses as $license ) {
11053 if ( $this->_user->id == $license->user_id ) {
11054 return true;
11055 }
11056 }
11057
11058 return false;
11059 }
11060
11061 /**
11062 * @author Vova Feldman (@svovaf)
11063 * @since 1.0.5
11064 *
11065 * @param bool|null $is_localhost
11066 *
11067 * @return FS_Plugin_License|false
11068 */
11069 function _get_available_premium_license( $is_localhost = null ) {
11070 $this->_logger->entrance();
11071
11072 $licenses = $this->get_available_premium_licenses( $is_localhost );
11073 if ( ! empty( $licenses ) ) {
11074 return $licenses[0];
11075 }
11076
11077 return false;
11078 }
11079
11080 /**
11081 * @author Vova Feldman (@svovaf)
11082 * @since 1.0.5
11083 *
11084 * @param bool|null $is_localhost
11085 *
11086 * @return FS_Plugin_License[]
11087 */
11088 function get_available_premium_licenses( $is_localhost = null ) {
11089 $this->_logger->entrance();
11090
11091 $licenses = array();
11092 if ( ! $this->has_paid_plan() ) {
11093 return $licenses;
11094 }
11095
11096 if ( is_array( $this->_licenses ) ) {
11097 foreach ( $this->_licenses as $license ) {
11098 if ( ! $license->can_activate( $is_localhost ) ) {
11099 continue;
11100 }
11101
11102 $licenses[] = $license;
11103 }
11104 }
11105
11106 return $licenses;
11107 }
11108
11109 /**
11110 * Sync local plugin plans with remote server.
11111 *
11112 * 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).
11113 *
11114 * @author Vova Feldman (@svovaf)
11115 * @since 1.0.5
11116 *
11117 * @return FS_Plugin_Plan[]|object
11118 */
11119 function _sync_plans() {
11120 $plans = $this->_fetch_plugin_plans();
11121
11122 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11123 $plans_map = array();
11124 foreach ( $plans as $plan ) {
11125 $plans_map[ $plan->id ] = true;
11126 }
11127
11128 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11129
11130 foreach ( $plans_ids_to_keep as $plan_id ) {
11131 if ( isset( $plans_map[ $plan_id ] ) ) {
11132 continue;
11133 }
11134
11135 $missing_plan = self::_get_plan_by_id( $plan_id );
11136
11137 if ( is_object( $missing_plan ) ) {
11138 $plans[] = $missing_plan;
11139 }
11140 }
11141
11142 $this->_plans = $plans;
11143 $this->_store_plans();
11144 }
11145
11146 $this->do_action( 'after_plans_sync', $plans );
11147
11148 return $this->_plans;
11149 }
11150
11151 /**
11152 * Check if specified plan exists locally. If not, fetch it and store it.
11153 *
11154 * @author Vova Feldman (@svovaf)
11155 * @since 2.0.0
11156 *
11157 * @param number $plan_id
11158 *
11159 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11160 */
11161 private function sync_plan_if_not_exist( $plan_id ) {
11162 $plan = self::_get_plan_by_id( $plan_id );
11163
11164 if ( is_object( $plan ) ) {
11165 // Plan already exists.
11166 return $plan;
11167 }
11168
11169 $plan = $this->fetch_plan_by_id( $plan_id );
11170
11171 if ( $plan instanceof FS_Plugin_Plan ) {
11172 $this->_plans[] = $plan;
11173 $this->_store_plans();
11174
11175 return $plan;
11176 }
11177
11178 return $plan;
11179 }
11180
11181 /**
11182 * Check if specified license exists locally. If not, fetch it and store it.
11183 *
11184 * @author Vova Feldman (@svovaf)
11185 * @since 2.0.0
11186 *
11187 * @param number $license_id
11188 * @param string $license_key
11189 *
11190 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11191 */
11192 private function sync_license_if_not_exist( $license_id, $license_key ) {
11193 $license = $this->_get_license_by_id( $license_id );
11194
11195 if ( is_object( $license ) ) {
11196 // License already exists.
11197 return $license;
11198 }
11199
11200 $license = $this->fetch_license_by_key( $license_id, $license_key );
11201
11202 if ( $license instanceof FS_Plugin_License ) {
11203 $this->_licenses[] = $license;
11204
11205 $this->set_license( $license );
11206
11207 $this->_store_licenses();
11208
11209 return $license;
11210 }
11211
11212 return $license;
11213 }
11214
11215 /**
11216 * Get a collection of unique plan IDs that are associated with any installs in the network.
11217 *
11218 * @author Leo Fajardo (@leorw)
11219 * @since 2.0.0
11220 *
11221 * @return number[]
11222 */
11223 private function get_plans_ids_associated_with_installs() {
11224 if ( ! is_multisite() ) {
11225 if ( ! is_object( $this->_site ) ||
11226 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11227 ) {
11228 return array();
11229 }
11230
11231 return array( $this->_site->plan_id );
11232 }
11233
11234 $plan_ids = array();
11235 $sites = self::get_sites();
11236 foreach ( $sites as $site ) {
11237 $blog_id = self::get_site_blog_id( $site );
11238 $install = $this->get_install_by_blog_id( $blog_id );
11239
11240 if ( ! is_object( $install ) ||
11241 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11242 ) {
11243 continue;
11244 }
11245
11246 $plan_ids[ $install->plan_id ] = true;
11247 }
11248
11249 return array_keys( $plan_ids );
11250 }
11251
11252 /**
11253 * Get a collection of unique license IDs that are associated with any installs in the network.
11254 *
11255 * @author Leo Fajardo (@leorw)
11256 * @since 2.0.0
11257 *
11258 * @return number[]
11259 */
11260 private function get_license_ids_associated_with_installs() {
11261 if ( ! $this->_is_network_active ) {
11262 if ( ! is_object( $this->_site ) ||
11263 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11264 ) {
11265 return array();
11266 }
11267
11268 return array( $this->_site->license_id );
11269 }
11270
11271 $license_ids = array();
11272 $sites = self::get_sites();
11273 foreach ( $sites as $site ) {
11274 $blog_id = self::get_site_blog_id( $site );
11275 $install = $this->get_install_by_blog_id( $blog_id );
11276
11277 if ( ! is_object( $install ) ||
11278 ! FS_Plugin_License::is_valid_id( $install->license_id )
11279 ) {
11280 continue;
11281 }
11282
11283 $license_ids[ $install->license_id ] = true;
11284 }
11285
11286 return array_keys( $license_ids );
11287 }
11288
11289 /**
11290 * @author Vova Feldman (@svovaf)
11291 * @since 1.0.5
11292 *
11293 * @param number $id
11294 *
11295 * @return FS_Plugin_Plan|false
11296 */
11297 function _get_plan_by_id( $id ) {
11298 $this->_logger->entrance();
11299
11300 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11301 $this->_sync_plans();
11302 }
11303
11304 foreach ( $this->_plans as $plan ) {
11305 if ( $id == $plan->id ) {
11306 return $plan;
11307 }
11308 }
11309
11310 return false;
11311 }
11312
11313 /**
11314 * @author Vova Feldman (@svovaf)
11315 * @since 1.1.8.1
11316 *
11317 * @param string $name
11318 *
11319 * @return FS_Plugin_Plan|false
11320 */
11321 private function get_plan_by_name( $name ) {
11322 $this->_logger->entrance();
11323
11324 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11325 $this->_sync_plans();
11326 }
11327
11328 foreach ( $this->_plans as $plan ) {
11329 if ( $name == $plan->name ) {
11330 return $plan;
11331 }
11332 }
11333
11334 return false;
11335 }
11336
11337 /**
11338 * Sync local licenses with remote server.
11339 *
11340 * @author Vova Feldman (@svovaf)
11341 * @since 1.0.6
11342 *
11343 * @param number|bool $site_license_id
11344 * @param number|null $blog_id
11345 *
11346 * @return FS_Plugin_License[]|object
11347 */
11348 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11349 $this->_logger->entrance();
11350
11351 $is_network_admin = fs_is_network_admin();
11352
11353 if ( $is_network_admin && is_null( $blog_id ) ) {
11354 $all_licenses = self::get_all_licenses( $this->_module_id );
11355 } else {
11356 $all_licenses = $this->get_user_licenses( $this->_user->id );
11357 }
11358
11359 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11360
11361 $all_licenses_map = array();
11362 foreach ( $all_licenses as $license ) {
11363 $all_licenses_map[ $license->id ] = true;
11364 }
11365
11366 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11367
11368 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11369 $licenses_map = array();
11370 foreach ( $licenses as $license ) {
11371 $licenses_map[ $license->id ] = true;
11372 }
11373
11374 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11375 // foreach ( $license_ids_to_keep as $license_id ) {
11376 // if ( isset( $licenses_map[ $license_id ] ) ) {
11377 // continue;
11378 // }
11379 //
11380 // $missing_license = self::_get_license_by_id( $license_id, false );
11381 // if ( is_object( $missing_license ) ) {
11382 // $licenses[] = $missing_license;
11383 // $licenses_map[ $missing_license->id ] = true;
11384 // }
11385 // }
11386
11387 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11388
11389 foreach ( $user_license_ids as $key => $license_id ) {
11390 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11391 // Remove access to licenses that no longer exist.
11392 unset( $user_license_ids[ $key ] );
11393 }
11394 }
11395
11396 if ( ! empty( $user_license_ids ) ) {
11397 foreach ( $licenses_map as $license_id => $value ) {
11398 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11399 // Associate new licenses with the user who triggered the license syncing.
11400 $user_license_ids[] = $license_id;
11401 }
11402 }
11403
11404 $user_license_ids = array_unique( $user_license_ids );
11405 } else {
11406 $user_license_ids = array_keys( $licenses_map );
11407 }
11408
11409 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11410 $user_licenses = array();
11411 foreach ( $licenses as $license ) {
11412 if ( ! in_array( $license->id, $user_license_ids ) ) {
11413 continue;
11414 }
11415
11416 $user_licenses[] = $license;
11417 }
11418
11419 $this->_licenses = $user_licenses;
11420 } else {
11421 $this->_licenses = $licenses;
11422 }
11423
11424 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11425
11426 $this->_store_licenses( true, $this->_module_id, $licenses );
11427 }
11428
11429 // Update current license.
11430 if ( is_object( $this->_license ) ) {
11431 $license = $this->_get_license_by_id( $this->_license->id );
11432
11433 if ( is_object( $license ) ) {
11434 /**
11435 * `$license` can be `false` in case a user change action has just been completed and this method
11436 * has synced the `$this->_licenses` collection for the new user. In this case, the
11437 * `$this->_licenses` collection may have only the newly activated license that is associated with
11438 * the new user. `set_license` will eventually be called in the same request by the logic that
11439 * follows outside this method which will detect that the install's license has been updated, and
11440 * then `_update_site_license` will be called which in turn will call `set_license`.
11441 *
11442 * @author Leo Fajardo (@leorw)
11443 * @since 2.3.2
11444 */
11445 $this->set_license( $license );
11446 }
11447 }
11448
11449 return $this->_licenses;
11450 }
11451
11452 /**
11453 * @author Vova Feldman (@svovaf)
11454 * @since 1.0.5
11455 *
11456 * @param number $id
11457 * @param bool $sync_licenses
11458 *
11459 * @return FS_Plugin_License|false
11460 */
11461 function _get_license_by_id( $id, $sync_licenses = true ) {
11462 $this->_logger->entrance();
11463
11464 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11465 return false;
11466 }
11467
11468 /**
11469 * When running from the network level admin and opted-in from the network,
11470 * check if the license exists in the network user licenses collection.
11471 *
11472 * @author Vova Feldman (@svovaf)
11473 * @since 2.0.0
11474 */
11475 if ( fs_is_network_admin() &&
11476 $this->is_network_registered() &&
11477 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11478 ) {
11479 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11480
11481 foreach ( $licenses as $license ) {
11482 if ( $id == $license->id ) {
11483 return $license;
11484 }
11485 }
11486 }
11487
11488 if ( ! $this->has_any_license() && $sync_licenses ) {
11489 $this->_sync_licenses( $id );
11490 }
11491
11492 if ( is_array( $this->_licenses ) ) {
11493 foreach ( $this->_licenses as $license ) {
11494 if ( $id == $license->id ) {
11495 return $license;
11496 }
11497 }
11498 }
11499
11500 return false;
11501 }
11502
11503 /**
11504 * 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.
11505 *
11506 * @author Vova Feldman (@svovaf)
11507 * @since 2.0.0
11508 *
11509 * @param number $id
11510 *
11511 * @return FS_Plugin_License
11512 */
11513 private function get_license_by_id( $id ) {
11514 $licenses = self::get_all_licenses( $this->_module_id );
11515
11516 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11517 foreach ( $licenses as $license ) {
11518 if ( $id == $license->id ) {
11519 return $license;
11520 }
11521 }
11522 }
11523
11524 return null;
11525 }
11526
11527 /**
11528 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11529 *
11530 * @author Vova Feldman (@svovaf)
11531 * @since 2.0.0
11532 *
11533 * @return \FS_Plugin_License|mixed
11534 */
11535 private function sync_site_license() {
11536 $api = $this->get_api_user_scope();
11537
11538 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11539
11540 if ( ! $this->is_api_result_entity( $result ) ) {
11541 return $result;
11542 }
11543
11544 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11545 $this->_store_licenses();
11546
11547 return $license;
11548 }
11549
11550 /**
11551 * Get all user's available licenses for the current module.
11552 *
11553 * @author Vova Feldman (@svovaf)
11554 * @since 2.0.0
11555 *
11556 * @param number $user_id
11557 *
11558 * @return FS_Plugin_License[]
11559 */
11560 private function get_user_licenses( $user_id ) {
11561 $all_licenses = self::get_all_licenses( $this->_module_id );
11562 if ( empty( $all_licenses ) ) {
11563 return array();
11564 }
11565
11566 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
11567 if ( empty( $user_license_ids ) ) {
11568 return array();
11569 }
11570
11571 $licenses = array();
11572 foreach ( $all_licenses as $license ) {
11573 if ( in_array( $license->id, $user_license_ids ) ) {
11574 $licenses[] = $license;
11575 }
11576 }
11577
11578 return $licenses;
11579 }
11580
11581 /**
11582 * Checks if the context license is network activated except on the given blog ID.
11583 *
11584 * @author Vova Feldman (@svovaf)
11585 * @since 2.0.0
11586 *
11587 * @param int $except_blog_id
11588 *
11589 * @return bool
11590 */
11591 private function is_license_network_active( $except_blog_id = 0 ) {
11592 $this->_logger->entrance();
11593
11594 if ( ! is_object( $this->_license ) ) {
11595 return false;
11596 }
11597
11598 $sites = self::get_sites();
11599
11600 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
11601 // There are more sites than the number of activations, so license cannot be network activated.
11602 return false;
11603 }
11604
11605 foreach ( $sites as $site ) {
11606 $blog_id = self::get_site_blog_id( $site );
11607
11608 if ( $except_blog_id == $blog_id ) {
11609 // Skip excluded blog.
11610 continue;
11611 }
11612
11613 $install = $this->get_install_by_blog_id( $blog_id );
11614
11615 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
11616 return false;
11617 }
11618 }
11619
11620 return true;
11621 }
11622
11623 /**
11624 * 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.
11625 *
11626 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
11627 *
11628 * @author Vova Feldman (@svovaf)
11629 * @since 2.0.0
11630 *
11631 * @param \FS_User $user
11632 * @param \FS_Plugin_License $license
11633 *
11634 * @return bool
11635 */
11636 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
11637 $this->_logger->entrance();
11638
11639 $result = $this->can_activate_license_on_network( $license );
11640
11641 if ( false === $result ) {
11642 return false;
11643 }
11644
11645 $installs_without_license = $result['installs'];
11646 if ( ! empty( $installs_without_license ) ) {
11647 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
11648 }
11649
11650 $disconnected_site_ids = $result['sites'];
11651 if ( ! empty( $disconnected_site_ids ) ) {
11652 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
11653 }
11654
11655 $this->link_license_2_user( $license->id, $user->id );
11656
11657 // Sync license after activations.
11658 $license->activated += $result['production_count'];
11659 $license->activated_local += $result['localhost_count'];
11660
11661 // $this->_store_licenses()
11662
11663 return true;
11664 }
11665
11666 /**
11667 * Checks if the given license can be activated on the whole network.
11668 *
11669 * @author Vova Feldman (@svovaf)
11670 * @since 2.0.0
11671 *
11672 * @param \FS_Plugin_License $license
11673 *
11674 * @return false|array {
11675 * @type array[int]FS_Site $installs Blog ID to install map.
11676 * @type int[] $sites Non-connected blog IDs.
11677 * @type int $production_count Production sites count.
11678 * @type int $localhost_count Production sites count.
11679 * }
11680 */
11681 private function can_activate_license_on_network( FS_Plugin_License $license ) {
11682 $sites = self::get_sites();
11683
11684 $production_count = 0;
11685 $localhost_count = 0;
11686
11687 $installs_without_license = array();
11688 $disconnected_site_ids = array();
11689
11690 foreach ( $sites as $site ) {
11691 $blog_id = self::get_site_blog_id( $site );
11692 $install = $this->get_install_by_blog_id( $blog_id );
11693
11694 if ( is_object( $install ) ) {
11695 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
11696 // License already activated on the install.
11697 continue;
11698 }
11699
11700 $url = $install->url;
11701
11702 $installs_without_license[ $blog_id ] = $install;
11703 } else {
11704 $url = is_object( $site ) ?
11705 $site->siteurl :
11706 get_site_url( $blog_id );
11707
11708 $disconnected_site_ids[] = $blog_id;
11709 }
11710
11711 if ( FS_Site::is_localhost_by_address( $url ) ) {
11712 $localhost_count ++;
11713 } else {
11714 $production_count ++;
11715 }
11716 }
11717
11718 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
11719 return false;
11720 }
11721
11722 return array(
11723 'installs' => $installs_without_license,
11724 'sites' => $disconnected_site_ids,
11725 'production_count' => $production_count,
11726 'localhost_count' => $localhost_count,
11727 );
11728 }
11729
11730 /**
11731 * Activate a given license on a collection of installs.
11732 *
11733 * @author Vova Feldman (@svovaf)
11734 * @since 2.0.0
11735 *
11736 * @param \FS_User $user
11737 * @param string $license_key
11738 * @param array $blog_2_install_map {
11739 * @key int Blog ID.
11740 * @value FS_Site Blog's associated install.
11741 * }
11742 *
11743 * @return mixed|true
11744 */
11745 private function activate_license_on_many_installs(
11746 FS_User $user,
11747 $license_key,
11748 array $blog_2_install_map
11749 ) {
11750 $params = array(
11751 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
11752 );
11753
11754 $install_2_blog_map = array();
11755 foreach ( $blog_2_install_map as $blog_id => $install ) {
11756 $params[] = array( 'id' => $install->id );
11757
11758 $install_2_blog_map[ $install->id ] = $blog_id;
11759 }
11760
11761 $result = $this->get_api_user_scope_by_user( $user )->call(
11762 "plugins/{$this->_plugin->id}/installs.json",
11763 'PUT',
11764 $params
11765 );
11766
11767 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
11768 return $result;
11769 }
11770
11771 foreach ( $result->installs as $r_install ) {
11772 $install = new FS_Site( $r_install );
11773 $install->is_disconnected = false;
11774
11775 // Update install.
11776 $this->_store_site(
11777 true,
11778 $install_2_blog_map[ $r_install->id ],
11779 $install
11780 );
11781 }
11782
11783 return true;
11784 }
11785
11786 /**
11787 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
11788 *
11789 * @author Vova Feldman (@svovaf)
11790 * @since 2.3.1
11791 *
11792 * @param \FS_User $user
11793 * @param string $license_key
11794 *
11795 * @return true|mixed True if successful, otherwise, the API result.
11796 */
11797 private function activate_license_on_site( FS_User $user, $license_key ) {
11798 return $this->activate_license_on_many_sites( $user, $license_key );
11799 }
11800
11801 /**
11802 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
11803 *
11804 * @author Vova Feldman (@svovaf)
11805 * @since 2.0.0
11806 *
11807 * @param \FS_User $user
11808 * @param string $license_key
11809 * @param int[] $site_ids
11810 *
11811 * @return true|mixed True if successful, otherwise, the API result.
11812 */
11813 private function activate_license_on_many_sites(
11814 FS_User $user,
11815 $license_key,
11816 array $site_ids = array()
11817 ) {
11818 $sites = array();
11819 foreach ( $site_ids as $site_id ) {
11820 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
11821 }
11822
11823 // Install the plugin.
11824 $result = $this->create_installs_with_user(
11825 $user,
11826 $license_key,
11827 false,
11828 $sites,
11829 false,
11830 true
11831 );
11832
11833 if ( ! $this->is_api_result_entity( $result ) &&
11834 ! $this->is_api_result_object( $result, 'installs' )
11835 ) {
11836 return $result;
11837 }
11838
11839 $installs = array();
11840
11841 if ( $this->is_api_result_entity( $result ) ) {
11842 $install = new FS_Site( $result );
11843
11844 $this->_user = $user;
11845
11846 $this->_store_site( true, null, $install );
11847
11848 $this->_site = $install;
11849
11850 $this->reset_anonymous_mode();
11851 } else {
11852 foreach ( $result->installs as $install ) {
11853 $installs[] = new FS_Site( $install );
11854 }
11855
11856 // Map site addresses to their blog IDs.
11857 $address_to_blog_map = $this->get_address_to_blog_map();
11858
11859 $first_blog_id = null;
11860
11861 foreach ( $installs as $install ) {
11862 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
11863 $blog_id = $address_to_blog_map[ $address ];
11864
11865 $this->_store_site( true, $blog_id, $install );
11866
11867 $this->reset_anonymous_mode( $blog_id );
11868
11869 if ( is_null( $first_blog_id ) ) {
11870 $first_blog_id = $blog_id;
11871 }
11872 }
11873
11874 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
11875 $this->_storage->network_install_blog_id = $first_blog_id;
11876 }
11877 }
11878
11879 return true;
11880 }
11881
11882 /**
11883 * Sync site's license with user licenses.
11884 *
11885 * @author Vova Feldman (@svovaf)
11886 * @since 1.0.6
11887 *
11888 * @param FS_Plugin_License|null $new_license
11889 *
11890 * @return FS_Plugin_License|null
11891 */
11892 function _update_site_license( $new_license ) {
11893 $this->_logger->entrance();
11894
11895 /**
11896 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
11897 * accordingly so that it will also handle the case when an ownership change is done via license
11898 * activation.
11899 *
11900 * @author Leo Fajardo (@leorw)
11901 * @since 2.3.2
11902 */
11903 $this->set_license( $new_license );
11904
11905 if ( ! is_object( $new_license ) ) {
11906 $this->_site->license_id = null;
11907 $this->_sync_site_subscription( null );
11908
11909 return $this->_license;
11910 }
11911
11912 $this->_site->license_id = $this->_license->id;
11913
11914 if ( ! is_array( $this->_licenses ) ) {
11915 $this->_licenses = array();
11916 }
11917
11918 $is_license_found = false;
11919 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
11920 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
11921 $this->_licenses[ $i ] = $new_license;
11922
11923 $is_license_found = true;
11924 break;
11925 }
11926 }
11927
11928 // If new license just append.
11929 if ( ! $is_license_found ) {
11930 $this->_licenses[] = $new_license;
11931 }
11932
11933 $this->_sync_site_subscription( $new_license );
11934
11935 return $this->_license;
11936 }
11937
11938 /**
11939 * @author Vova Feldman (@svovaf)
11940 * @since 2.3.1
11941 *
11942 * @param \FS_Plugin_License $license
11943 */
11944 private function set_license( FS_Plugin_License $license = null ) {
11945 $this->_license = $license;
11946
11947 $this->maybe_update_whitelabel_flag( $license );
11948 }
11949
11950 /**
11951 * @author Leo Fajardo (@leorw)
11952 * @since 2.3.1
11953 *
11954 * @param FS_Plugin_License $license
11955 */
11956 private function maybe_update_whitelabel_flag( $license ) {
11957 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
11958 $this->_storage->is_whitelabeled :
11959 false;
11960
11961 if ( is_object( $license ) ) {
11962 $license_user = self::_get_user_by_id( $license->user_id );
11963
11964 if ( ! is_object( $license_user ) ) {
11965 // If foreign license, do not update the `is_whitelabeled` flag.
11966 return;
11967 }
11968
11969 if ( $this->is_addon() ) {
11970 /**
11971 * Store the last license data to the parent's storage since it's needed only when showing the
11972 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
11973 * iterate over the add-ons just to get the last license data.
11974 */
11975 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
11976 } else {
11977 $this->store_last_activated_license_data( $license );
11978 }
11979
11980 if ( $license->is_whitelabeled ) {
11981 // Activated a developer license, data should be hidden.
11982 $is_whitelabeled = true;
11983 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
11984 // The account owner activated a regular license key, no need to hide the data.
11985 $is_whitelabeled = false;
11986 }
11987 }
11988
11989 $this->_storage->is_whitelabeled = $is_whitelabeled;
11990
11991 // Reset the whitelabeled status after update.
11992 $this->is_whitelabeled = null;
11993 if ( $this->is_addon() ) {
11994 $parent_fs = $this->get_parent_instance();
11995
11996 if ( is_object( $parent_fs ) ) {
11997 $parent_fs->is_whitelabeled = null;
11998 }
11999 }
12000 }
12001
12002 /**
12003 * @author Leo Fajardo (@leorw)
12004 * @since 2.3.1
12005 *
12006 * @param FS_Plugin_License $license
12007 * @param FS_User $license_user
12008 */
12009 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12010 if ( ! is_object( $license_user ) ) {
12011 $this->_storage->last_license_key = md5( $license->secret_key );
12012 $this->_storage->last_license_user_id = null;
12013 } else {
12014 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12015 $this->_storage->last_license_user_id = $license_user->id;
12016 }
12017 }
12018
12019 /**
12020 * @author Leo Fajardo (@leorw)
12021 * @since 2.3.1
12022 *
12023 * @param bool $ignore_data_debug_mode
12024 *
12025 * @return bool
12026 */
12027 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12028 if ( true !== $this->_storage->is_whitelabeled ) {
12029 return false;
12030 } else if ( $ignore_data_debug_mode ) {
12031 return true;
12032 }
12033
12034 $fs = $this->is_addon() ?
12035 $this->get_parent_instance() :
12036 $this;
12037
12038 return ! $fs->is_data_debug_mode();
12039 }
12040
12041 /**
12042 * @author Leo Fajardo (@leorw)
12043 * @since 2.3.1
12044 *
12045 * @return number
12046 */
12047 function get_last_license_user_id() {
12048 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12049 $this->_storage->last_license_user_id :
12050 null;
12051 }
12052
12053 /**
12054 * @author Leo Fajardo (@leorw)
12055 * @since 2.3.1
12056 *
12057 * @param int $blog_id
12058 * @param bool $ignore_data_debug_mode
12059 *
12060 * @return bool
12061 */
12062 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12063 if ( ! is_null( $blog_id ) ) {
12064 $this->switch_to_blog( $blog_id );
12065 }
12066
12067 if ( ! is_null( $this->is_whitelabeled ) ) {
12068 $is_whitelabeled = $this->is_whitelabeled;
12069 } else {
12070 $is_whitelabeled = false;
12071
12072 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12073
12074 if ( ! $this->has_addons() ) {
12075 $is_whitelabeled = $is_whitelabeled_flag;
12076 } else if ( $is_whitelabeled_flag ) {
12077 $is_whitelabeled = true;
12078 } else {
12079 $addon_ids = $this->get_updated_account_addons();
12080 $installed_addons = $this->get_installed_addons();
12081 foreach ( $installed_addons as $fs_addon ) {
12082 $addon_ids[] = $fs_addon->get_id();
12083 }
12084
12085 if ( ! empty( $addon_ids ) ) {
12086 $addon_ids = array_unique( $addon_ids );
12087
12088 $is_network_level = (
12089 fs_is_network_admin() &&
12090 $this->is_network_active()
12091 );
12092
12093 foreach ( $addon_ids as $addon_id ) {
12094 $addon = $this->get_addon( $addon_id );
12095
12096 if ( ! is_object( $addon ) ) {
12097 continue;
12098 }
12099
12100 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12101 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12102 self::get_addon_instance( $addon_id ) :
12103 null;
12104
12105 $was_addon_network_activated = false;
12106
12107 if ( is_object( $fs_addon ) ) {
12108 $was_addon_network_activated = $fs_addon->is_network_active();
12109 } else if ( $is_network_level ) {
12110 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12111 }
12112
12113 $network_delegated_connection = (
12114 $was_addon_network_activated &&
12115 $addon_storage->get( 'is_delegated_connection', false, true )
12116 );
12117
12118 if (
12119 $is_network_level &&
12120 ( ! $was_addon_network_activated || $network_delegated_connection )
12121 ) {
12122 $sites = self::get_sites();
12123
12124 /**
12125 * If in network admin area and the add-on was not network-activated or network-activated
12126 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12127 */
12128 foreach ( $sites as $site ) {
12129 $site_info = $this->get_site_info( $site );
12130
12131 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12132 $is_whitelabeled = true;
12133 break;
12134 }
12135 }
12136
12137 if ( $is_whitelabeled ) {
12138 break;
12139 }
12140 } else {
12141 /**
12142 * This will be executed when any of the following is met:
12143 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12144 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12145 * 3. Add-on was not network-activated and in site admin area.
12146 */
12147 if ( true === $addon_storage->is_whitelabeled ) {
12148 $is_whitelabeled = true;
12149 break;
12150 }
12151 }
12152 }
12153 }
12154 }
12155
12156 $this->is_whitelabeled = $is_whitelabeled;
12157
12158 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12159 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12160 }
12161
12162 if ( ! is_null( $blog_id ) ) {
12163 $this->restore_current_blog();
12164 }
12165 }
12166
12167 return (
12168 $is_whitelabeled &&
12169 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12170 );
12171 }
12172
12173 /**
12174 * Sync site's subscription.
12175 *
12176 * @author Vova Feldman (@svovaf)
12177 * @since 1.0.9
12178 *
12179 * @param FS_Plugin_License|null $license
12180 *
12181 * @return bool|\FS_Subscription
12182 */
12183 private function _sync_site_subscription( $license ) {
12184 if ( ! is_object( $license ) ) {
12185 $this->delete_unused_subscriptions();
12186
12187 return false;
12188 }
12189
12190 // Load subscription details if not lifetime.
12191 $subscription = $license->is_lifetime() ?
12192 false :
12193 $this->_fetch_site_license_subscription();
12194
12195 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12196 $this->store_subscription( $subscription );
12197 } else {
12198 $this->delete_unused_subscriptions();
12199 }
12200
12201 return $subscription;
12202 }
12203
12204 /**
12205 * @author Vova Feldman (@svovaf)
12206 * @since 1.0.6
12207 *
12208 * @return bool|\FS_Plugin_License
12209 */
12210 function _get_license() {
12211 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12212 return $this->_license;
12213 }
12214
12215 return $this->_get_available_premium_license();
12216 }
12217
12218 /**
12219 * @param number $license_id
12220 *
12221 * @return null|\FS_Subscription
12222 */
12223 function _get_subscription( $license_id ) {
12224 if ( ! isset( $this->_storage->subscriptions ) ||
12225 empty( $this->_storage->subscriptions )
12226 ) {
12227 return null;
12228 }
12229
12230 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12231 if ( $subscription->license_id == $license_id ) {
12232 return $subscription;
12233 }
12234 }
12235
12236 return null;
12237 }
12238
12239 /**
12240 * @author Leo Fajardo (@leorw)
12241 * @since 2.0.0
12242 *
12243 * @param FS_Subscription $subscription
12244 */
12245 function store_subscription( FS_Subscription $subscription ) {
12246 if ( ! isset( $this->_storage->subscriptions ) ) {
12247 $this->_storage->subscriptions = array();
12248 }
12249
12250 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12251 $this->_storage->subscriptions = array( $subscription );
12252
12253 return;
12254 }
12255
12256 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12257
12258 $updated_subscription = false;
12259 foreach ( $subscriptions as $key => $existing_subscription ) {
12260 if ( $existing_subscription->id == $subscription->id ) {
12261 $subscriptions[ $key ] = $subscription;
12262 $updated_subscription = true;
12263 break;
12264 }
12265 }
12266
12267 if ( ! $updated_subscription ) {
12268 $subscriptions[] = $subscription;
12269 }
12270
12271 $this->_storage->subscriptions = $subscriptions;
12272 }
12273
12274 /**
12275 * @author Leo Fajardo (@leorw)
12276 * @since 2.0.0
12277 */
12278 function delete_unused_subscriptions() {
12279 if ( ! isset( $this->_storage->subscriptions ) ||
12280 empty( $this->_storage->subscriptions ) ||
12281 // Clean up only if there are already at least 3 subscriptions.
12282 ( count( $this->_storage->subscriptions ) < 3 )
12283 ) {
12284 return;
12285 }
12286
12287 if ( ! is_multisite() ) {
12288 // If not multisite, there should only be 1 subscription, so just clear the array.
12289 $this->_storage->subscriptions = array();
12290
12291 return;
12292 }
12293
12294 $subscriptions_to_keep_by_license_id_map = array();
12295 $sites = self::get_sites();
12296 foreach ( $sites as $site ) {
12297 $blog_id = self::get_site_blog_id( $site );
12298 $install = $this->get_install_by_blog_id( $blog_id );
12299
12300 if ( ! is_object( $install ) ||
12301 ! FS_Plugin_License::is_valid_id( $install->license_id )
12302 ) {
12303 continue;
12304 }
12305
12306 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12307 }
12308
12309 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12310 $this->_storage->subscriptions = array();
12311
12312 return;
12313 }
12314
12315 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12316 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12317 unset( $this->_storage->subscriptions[ $key ] );
12318 }
12319 }
12320 }
12321
12322 /**
12323 * @author Vova Feldman (@svovaf)
12324 * @since 1.0.2
12325 *
12326 * @param string $plan Plan name
12327 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12328 *
12329 * @return bool
12330 */
12331 function is_plan( $plan, $exact = false ) {
12332 $this->_logger->entrance();
12333
12334 if ( ! $this->is_registered() ) {
12335 return false;
12336 }
12337
12338 $plan = strtolower( $plan );
12339
12340 $current_plan_name = $this->get_plan_name();
12341
12342 if ( $current_plan_name === $plan ) {
12343 // Exact plan.
12344 return true;
12345 } else if ( $exact ) {
12346 // Required exact, but plans are different.
12347 return false;
12348 }
12349
12350 $current_plan_order = - 1;
12351 $required_plan_order = PHP_INT_MAX;
12352 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12353 if ( $plan === $this->_plans[ $i ]->name ) {
12354 $required_plan_order = $i;
12355 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12356 $current_plan_order = $i;
12357 }
12358 }
12359
12360 return ( $current_plan_order > $required_plan_order );
12361 }
12362
12363 /**
12364 * Check if module has only one plan.
12365 *
12366 * @author Vova Feldman (@svovaf)
12367 * @since 1.2.1.7
12368 *
12369 * @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.
12370 *
12371 * @return bool
12372 */
12373 function is_single_plan( $double_check = false ) {
12374 $this->_logger->entrance();
12375
12376 if ( ! $this->is_registered() ||
12377 ! is_array( $this->_plans ) ||
12378 0 === count( $this->_plans )
12379 ) {
12380 return true;
12381 }
12382
12383 $has_free_plan = $this->has_free_plan();
12384
12385 if ( ! $has_free_plan && $double_check ) {
12386 foreach ( $this->_plans as $plan ) {
12387 if ( $plan->is_free() ) {
12388 $has_free_plan = true;
12389 break;
12390 }
12391 }
12392 }
12393
12394 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12395 }
12396
12397 /**
12398 * Check if plan based on trial. If not in trial mode, should return false.
12399 *
12400 * @since 1.0.9
12401 *
12402 * @param string $plan Plan name
12403 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12404 *
12405 * @return bool
12406 */
12407 function is_trial_plan( $plan, $exact = false ) {
12408 $this->_logger->entrance();
12409
12410 if ( ! $this->is_registered() ) {
12411 return false;
12412 }
12413
12414 if ( ! $this->is_trial() ) {
12415 return false;
12416 }
12417
12418 $trial_plan = $this->get_trial_plan();
12419
12420 if ( $trial_plan->name === $plan ) {
12421 // Exact plan.
12422 return true;
12423 } else if ( $exact ) {
12424 // Required exact, but plans are different.
12425 return false;
12426 }
12427
12428 $current_plan_order = - 1;
12429 $required_plan_order = - 1;
12430 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12431 if ( $plan === $this->_plans[ $i ]->name ) {
12432 $required_plan_order = $i;
12433 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12434 $current_plan_order = $i;
12435 }
12436 }
12437
12438 return ( $current_plan_order > $required_plan_order );
12439 }
12440
12441 /**
12442 * Check if plugin has any paid plans.
12443 *
12444 * @author Vova Feldman (@svovaf)
12445 * @since 1.0.7
12446 *
12447 * @return bool
12448 */
12449 function has_paid_plan() {
12450 return $this->_has_paid_plans ||
12451 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12452 }
12453
12454 /**
12455 * Check if plugin has any plan with a trail.
12456 *
12457 * @author Vova Feldman (@svovaf)
12458 * @since 1.0.9
12459 *
12460 * @return bool
12461 */
12462 function has_trial_plan() {
12463 /**
12464 * @author Vova Feldman(@svovaf)
12465 * @since 1.2.1.5
12466 *
12467 * Allow setting a trial from the SDK without calling the API.
12468 * But, if the user did opt-in, continue using the real data from the API.
12469 */
12470 if ( $this->_trial_days >= 0 ) {
12471 return true;
12472 }
12473
12474 return $this->_storage->get( 'has_trial_plan', false );
12475 }
12476
12477 /**
12478 * Check if plugin has any free plan, or is it premium only.
12479 *
12480 * Note: If no plans configured, assume plugin is free.
12481 *
12482 * @author Vova Feldman (@svovaf)
12483 * @since 1.0.7
12484 *
12485 * @return bool
12486 */
12487 function has_free_plan() {
12488 return ! $this->is_only_premium();
12489 }
12490
12491 /**
12492 * Displays a license activation dialog box when the user clicks on the "Activate License"
12493 * or "Change License" link on the plugins
12494 * page.
12495 *
12496 * @author Leo Fajardo (@leorw)
12497 * @since 1.1.9
12498 */
12499 function _add_license_activation_dialog_box() {
12500 $vars = array(
12501 'id' => $this->_module_id,
12502 );
12503
12504 fs_require_template( 'forms/license-activation.php', $vars );
12505 fs_require_template( 'forms/resend-key.php', $vars );
12506 }
12507
12508 /**
12509 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
12510 *
12511 * @author Leo Fajardo (@leorw)
12512 * @since 2.3.2
12513 *
12514 * @return number[]
12515 */
12516 function get_installs_ids_with_foreign_licenses() {
12517 $installs = array();
12518
12519 if (
12520 is_object( $this->_license ) &&
12521 $this->_site->user_id != $this->_license->user_id
12522 ) {
12523 $installs[] = $this->_site->id;
12524 }
12525
12526 /**
12527 * Also try to get foreign licenses for the context product's add-ons.
12528 */
12529 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
12530
12531 foreach ( $installs_by_slug_map as $slug => $install_info ) {
12532 if ( $slug == $this->get_slug() ) {
12533 continue;
12534 }
12535
12536 $install = $install_info['install'];
12537 $license = $install_info['license'];
12538
12539 if (
12540 is_object( $license ) &&
12541 $install->user_id != $license->user_id
12542 ) {
12543 $installs[] = $install->id;
12544 }
12545 }
12546
12547 return $installs;
12548 }
12549
12550 /**
12551 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
12552 *
12553 * @author Leo Fajardo (@leorw)
12554 * @since 2.3.2
12555 *
12556 * @param number[] $install_ids
12557 */
12558 function _add_user_change_dialog_box( $install_ids ) {
12559 $vars = array(
12560 'id' => $this->_module_id,
12561 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
12562 );
12563
12564 fs_require_template( 'forms/user-change.php', $vars );
12565 }
12566
12567 /**
12568 * @author Leo Fajardo (@leorw)
12569 * @since 2.3.1
12570 */
12571 function _add_data_debug_mode_dialog_box() {
12572 $vars = array(
12573 'id' => $this->_module_id,
12574 );
12575
12576 fs_require_template( 'forms/data-debug-mode.php', $vars );
12577 }
12578
12579 /**
12580 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
12581 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
12582 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
12583 * is only activated on a single production site.
12584 *
12585 * @author Leo Fajardo (@leorw)
12586 * @since 2.2.1
12587 *
12588 * @param bool $is_license_deactivation
12589 *
12590 * @return array
12591 */
12592 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
12593 if ( fs_is_network_admin() ) {
12594 // Subscription cancellation dialog box is currently not supported for multisite networks.
12595 return array();
12596 }
12597
12598 if ( $this->is_whitelabeled() ) {
12599 return array();
12600 }
12601
12602 $license = $this->_get_license();
12603
12604 /**
12605 * 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.
12606 *
12607 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
12608 * @since 2.2.1
12609 */
12610 if ( ! is_object( $license ) ||
12611 $license->is_lifetime() ||
12612 ( ! $license->is_single_site() && $license->activated > 1 )
12613 ) {
12614 return array();
12615 }
12616
12617 /**
12618 * @var FS_Subscription $subscription
12619 */
12620 $subscription = $this->_get_subscription( $license->id );
12621 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
12622 return array();
12623 }
12624
12625 return array(
12626 'id' => $this->_module_id,
12627 'license' => $license,
12628 'has_trial' => $this->is_paid_trial(),
12629 'is_license_deactivation' => $is_license_deactivation,
12630 );
12631 }
12632
12633 /**
12634 * @author Leo Fajardo (@leorw)
12635 * @since 2.0.2
12636 */
12637 function _add_premium_version_upgrade_selection_dialog_box() {
12638 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
12639 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
12640 return;
12641 }
12642
12643 $vars = array(
12644 'id' => $this->_module_id,
12645 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
12646 $modules_update->response[ $this->_plugin_basename ]->new_version :
12647 $modules_update->response[ $this->_plugin_basename ]['new_version']
12648 );
12649
12650 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
12651 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
12652 }
12653
12654 /**
12655 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
12656 * page.
12657 *
12658 * @author Leo Fajardo (@leorw)
12659 * @since 1.2.1.5
12660 */
12661 function _add_optout_dialog() {
12662 if ( $this->is_theme() ) {
12663 $vars = null;
12664 fs_require_once_template( '/js/jquery.content-change.php', $vars );
12665 }
12666
12667 $vars = array( 'id' => $this->_module_id );
12668 fs_require_template( 'forms/optout.php', $vars );
12669 }
12670
12671 /**
12672 * Prepare page to include all required UI and logic for the license activation dialog.
12673 *
12674 * @author Vova Feldman (@svovaf)
12675 * @since 1.2.0
12676 */
12677 function _add_license_activation() {
12678 if ( ! $this->is_user_admin() ) {
12679 // Only admins can activate a license.
12680 return;
12681 }
12682
12683 if ( ! $this->has_paid_plan() ) {
12684 // Module doesn't have any paid plans.
12685 return;
12686 }
12687
12688 if (
12689 $this->has_premium_version() &&
12690 ! $this->is_premium() &&
12691 /**
12692 * Also handle the case when an upgrade was made using the free version.
12693 *
12694 * @author Leo Fajardo (@leorw)
12695 * @since 2.3.2
12696 */
12697 ! is_object( $this->_get_license() )
12698 ) {
12699 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
12700 return;
12701 }
12702
12703 // Add license activation link and AJAX request handler.
12704 if ( self::is_plugins_page() ) {
12705 $is_network_admin = fs_is_network_admin();
12706
12707 if (
12708 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
12709 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
12710 ) {
12711 /**
12712 * @since 1.2.0 Add license action link only on plugins page.
12713 */
12714 $this->_add_license_action_link();
12715 }
12716 }
12717
12718 // Add license activation AJAX callback.
12719 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
12720
12721 // Add resend license AJAX callback.
12722 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
12723 }
12724
12725 /**
12726 * Prepares page to include all required UI and logic for the "Change User" dialog.
12727 *
12728 * @author Leo Fajardo (@leorw)
12729 * @since 2.3.2
12730 */
12731 function _add_user_change_option() {
12732 if ( ! $this->should_handle_user_change() ) {
12733 return;
12734 }
12735
12736 if ( empty( $this->get_installs_ids_with_foreign_licenses() ) ) {
12737 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
12738 return;
12739 }
12740
12741 // Add user change AJAX handler.
12742 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
12743 }
12744
12745 /**
12746 * @author Leo Fajardo (@leorw)
12747 * @since 2.3.2
12748 */
12749 function should_handle_user_change() {
12750 if ( ! $this->is_user_admin() ) {
12751 // Only admins can change user.
12752 return false;
12753 }
12754
12755 if ( $this->is_addon() ) {
12756 return false;
12757 }
12758
12759 if ( ! $this->is_registered() ) {
12760 return false;
12761 }
12762
12763 if (
12764 $this->is_network_active() &&
12765 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
12766 ) {
12767 // Handle only on site-level "Account" section for now.
12768 return false;
12769 }
12770
12771 return true;
12772 }
12773
12774 /**
12775 * @author Leo Fajardo (@leorw)
12776 * @since 2.0.2
12777 */
12778 function _add_premium_version_upgrade_selection() {
12779 if ( ! $this->is_user_admin() ) {
12780 return;
12781 }
12782
12783 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
12784 // This is relevant only to the free versions and premium versions without an active license.
12785 return;
12786 }
12787
12788 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
12789 $this->_add_premium_version_upgrade_selection_action();
12790 }
12791 }
12792
12793 /**
12794 * @author Leo Fajardo (@leorw)
12795 * @since 2.3.0
12796 */
12797 function _add_beta_mode_update_handler() {
12798 if ( ! $this->is_user_admin() ) {
12799 return;
12800 }
12801
12802 if ( ! $this->is_premium() ) {
12803 return;
12804 }
12805
12806 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
12807 }
12808
12809 /**
12810 * @author Leo Fajardo (@leorw)
12811 * @since 2.3.0
12812 */
12813 function _set_beta_mode_ajax_handler() {
12814 $this->_logger->entrance();
12815
12816 $this->check_ajax_referer( 'set_beta_mode' );
12817
12818 if ( ! $this->is_user_admin() ) {
12819 // Only for admins.
12820 self::shoot_ajax_failure();
12821 }
12822
12823 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
12824
12825 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
12826 self::shoot_ajax_failure();
12827 }
12828
12829 $user = $this->get_api_user_scope()->call(
12830 '',
12831 'put',
12832 array(
12833 'plugin_id' => $this->get_id(),
12834 'is_beta' => ( 'true' == $is_beta ),
12835 'fields' => 'is_beta'
12836 )
12837 );
12838
12839 if ( ! $this->is_api_result_entity( $user ) ) {
12840 self::shoot_ajax_failure(
12841 FS_Api::is_api_error_object( $user ) ?
12842 $user->error->message :
12843 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
12844 );
12845 }
12846
12847 $this->_user->is_beta = $user->is_beta;
12848 $this->_store_user();
12849
12850 self::shoot_ajax_response( array( 'success' => true ) );
12851 }
12852
12853 /**
12854 * License activation WP AJAX handler.
12855 *
12856 * @author Leo Fajardo (@leorw)
12857 * @since 1.1.9
12858 *
12859 * @uses Freemius::activate_license()
12860 */
12861 function _activate_license_ajax_action() {
12862 $this->_logger->entrance();
12863
12864 $this->check_ajax_referer( 'activate_license' );
12865
12866 $license_key = trim( fs_request_get( 'license_key' ) );
12867
12868 if ( empty( $license_key ) ) {
12869 exit;
12870 }
12871
12872 $result = $this->activate_license(
12873 $license_key,
12874 fs_is_network_admin() ?
12875 fs_request_get( 'sites', array(), 'post' ) :
12876 array(),
12877 fs_request_get_bool( 'is_marketing_allowed', null ),
12878 fs_request_get( 'blog_id', null ),
12879 fs_request_get( 'module_id', null, 'post' ),
12880 fs_request_get( 'user_id', null ),
12881 fs_request_get_bool( 'is_extensions_tracking_allowed', true )
12882 );
12883
12884 echo json_encode( $result );
12885
12886 exit;
12887 }
12888
12889 /**
12890 * User change WP AJAX handler.
12891 *
12892 * @author Leo Fajardo (@leorw)
12893 * @since 2.3.2
12894 */
12895 function _user_change_ajax_action() {
12896 $this->_logger->entrance();
12897
12898 $this->check_ajax_referer( 'change_user' );
12899
12900 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
12901 $new_user_id = fs_request_get( 'user_id' );
12902
12903 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
12904 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
12905 }
12906
12907 $params = array();
12908
12909 if ( ! empty( $new_email_address ) ) {
12910 $params['user_email'] = $new_email_address;
12911 } else {
12912 $params['user_id'] = $new_user_id;
12913 }
12914
12915 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
12916 $install_ids = array();
12917
12918 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
12919 $install_ids[ $slug ] = $install_info['install']->id;
12920 }
12921
12922 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
12923
12924 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
12925
12926 if ( FS_Api::is_api_error( $install ) ) {
12927 $error = '';
12928
12929 if ( is_object( $install ) ) {
12930 switch ( $install->error->code ) {
12931 case 'user_exist':
12932 $error = (
12933 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
12934 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
12935 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>' ) .
12936 sprintf(
12937 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
12938 $this->get_account_url( 'change_owner', array(
12939 'state' => 'init',
12940 'candidate_email' => $new_email_address
12941 ) ),
12942 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
12943 )
12944 );
12945 break;
12946 }
12947 }
12948
12949 if ( empty( $error ) ) {
12950 $error = FS_Api::is_api_error_object( $install ) ?
12951 $install->error->message :
12952 var_export( $install->error, true );
12953 }
12954
12955 self::shoot_ajax_failure( $error );
12956 } else {
12957 if (
12958 // If successful ownership change.
12959 $this->get_user()->id != $install->user_id ||
12960 ! empty( $new_email_address )
12961 ) {
12962 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
12963 }
12964 }
12965
12966 self::shoot_ajax_success();
12967 }
12968
12969 /**
12970 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
12971 *
12972 * @author Vova Feldman (@svovaf)
12973 * @since 2.3.0
12974 *
12975 * @param string $license_key
12976 * @param null|bool $is_marketing_allowed
12977 * @param null|number $plugin_id
12978 *
12979 * @return array {
12980 * @var bool $success
12981 * @var string $error
12982 * @var string $next_page
12983 * }
12984 *
12985 * @uses Freemius::activate_license()
12986 */
12987 function activate_migrated_license(
12988 $license_key,
12989 $is_marketing_allowed = null,
12990 $plugin_id = null
12991 ) {
12992 $this->_logger->entrance();
12993
12994 $result = $this->activate_license(
12995 $license_key,
12996 $this->is_network_active() ?
12997 $this->get_sites_for_network_level_optin() :
12998 array(),
12999 $is_marketing_allowed,
13000 null,
13001 $plugin_id
13002 );
13003
13004 // No need to show the sticky after license activation notice after migrating a license.
13005 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13006
13007 return $result;
13008 }
13009
13010 /**
13011 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13012 *
13013 * @author Vova Feldman (@svovaf)
13014 * @since 2.2.4
13015 * @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).
13016 * @param string $license_key
13017 * @param array $sites
13018 * @param null|bool $is_marketing_allowed
13019 * @param null|int $blog_id
13020 * @param null|number $plugin_id
13021 * @param null|number $license_owner_id
13022 *
13023 * @return array {
13024 * @var bool $success
13025 * @var string $error
13026 * @var string $next_page
13027 * }
13028 */
13029 private function activate_license(
13030 $license_key,
13031 $sites = array(),
13032 $is_marketing_allowed = null,
13033 $blog_id = null,
13034 $plugin_id = null,
13035 $license_owner_id = null,
13036 $is_extensions_tracking_allowed = true
13037 ) {
13038 $this->_logger->entrance();
13039
13040 $license_key = trim( $license_key );
13041
13042 if ( ! fs_is_network_admin() ) {
13043 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13044 $sites = array();
13045 }
13046
13047 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13048 $this :
13049 $this->get_addon_instance( $plugin_id );
13050
13051 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
13052
13053 $error = false;
13054 $next_page = false;
13055
13056 $has_valid_blog_id = is_numeric( $blog_id );
13057
13058 $user = null;
13059
13060 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13061 /**
13062 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13063 *
13064 * @author Vova Feldman (@svovaf)
13065 */
13066 $user = $fs->get_parent_instance()->get_current_or_network_user();
13067 } else if ( $fs->is_registered() ) {
13068 $user = $fs->get_current_or_network_user();
13069 }
13070
13071 if ( is_object( $user ) ) {
13072 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13073 // If no specific blog ID was provided, activate the license for all sites in the network.
13074 $blog_2_install_map = array();
13075 $site_ids = array();
13076
13077 foreach ( $sites as $site ) {
13078 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13079 continue;
13080 }
13081
13082 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13083
13084 if ( is_object( $install ) ) {
13085 $blog_2_install_map[ $site['blog_id'] ] = $install;
13086 } else {
13087 $site_ids[] = $site['blog_id'];
13088 }
13089 }
13090
13091 if ( ! empty( $blog_2_install_map ) ) {
13092 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13093
13094 if ( true !== $result ) {
13095 $error = FS_Api::is_api_error_object( $result ) ?
13096 $result->error->message :
13097 var_export( $result, true );
13098 }
13099 }
13100
13101 if ( empty( $error ) && ! empty( $site_ids ) ) {
13102 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13103
13104 if ( true !== $result ) {
13105 $error = FS_Api::is_api_error_object( $result ) ?
13106 $result->error->message :
13107 var_export( $result, true );
13108 }
13109 }
13110 } else {
13111 if ( $has_valid_blog_id ) {
13112 /**
13113 * If a specific blog ID was provided, activate the license only for the install that is
13114 * associated with the given blog ID.
13115 *
13116 * @author Leo Fajardo (@leorw)
13117 */
13118 $fs->switch_to_blog( $blog_id );
13119 }
13120
13121 if ( $fs->is_registered() ) {
13122 $params = array(
13123 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13124 );
13125
13126 $install_ids = array();
13127
13128 $change_owner = FS_User::is_valid_id( $license_owner_id );
13129
13130 if ( $change_owner ) {
13131 $params['user_id'] = $license_owner_id;
13132
13133 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13134
13135 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13136 $install_ids[ $slug ] = $install_info['install']->id;
13137 }
13138
13139 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13140 }
13141
13142 $api = $fs->get_api_site_scope();
13143
13144 $install = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13145
13146 if ( FS_Api::is_api_error( $install ) ) {
13147 $error = FS_Api::is_api_error_object( $install ) ?
13148 $install->error->message :
13149 var_export( $install->error, true );
13150 } else {
13151 $fs->reconnect_locally( $has_valid_blog_id );
13152
13153 if (
13154 $change_owner &&
13155 // If successful ownership change.
13156 $fs->get_user()->id != $install->user_id
13157 ) {
13158 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13159 }
13160 }
13161 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13162 $result = $fs->activate_license_on_site( $user, $license_key );
13163
13164 if ( true !== $result ) {
13165 $error = FS_Api::is_api_error_object( $result ) ?
13166 $result->error->message :
13167 var_export( $result, true );
13168 }
13169 }
13170 }
13171
13172 if ( empty( $error ) ) {
13173 $fs->network_upgrade_mode_completed();
13174
13175 $fs->_sync_license( true, $has_valid_blog_id );
13176
13177 $this->maybe_sync_install_user();
13178
13179 $next_page = $fs->is_addon() ?
13180 $fs->get_parent_instance()->get_account_url() :
13181 $fs->get_account_url();
13182 }
13183 } else {
13184 $next_page = $fs->opt_in(
13185 false,
13186 false,
13187 false,
13188 $license_key,
13189 false,
13190 false,
13191 false,
13192 $is_marketing_allowed,
13193 $sites
13194 );
13195
13196 if ( isset( $next_page->error ) ) {
13197 $error = $next_page->error;
13198 } else {
13199 if ( fs_is_network_admin() ) {
13200 /**
13201 * Get the list of sites that were just opted-in (and license activated).
13202 * This is an optimization for the next part below saving some DB queries.
13203 */
13204 $connected_sites = array();
13205 foreach ( $sites as $site ) {
13206 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13207 $connected_sites[ $site['blog_id'] ] = true;
13208 }
13209 }
13210
13211 $all_sites = self::get_sites();
13212 $pending_sites = array();
13213
13214 /**
13215 * 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.
13216 *
13217 * @author Vova Feldman (@svovaf)
13218 */
13219 foreach ( $all_sites as $site ) {
13220 $blog_id = self::get_site_blog_id( $site );
13221
13222 if ( isset( $connected_sites[ $blog_id ] ) ) {
13223 // Site was just connected.
13224 continue;
13225 }
13226
13227 if ( $fs->is_installed_on_site( $blog_id ) ) {
13228 // Site was already connected before.
13229 continue;
13230 }
13231
13232 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13233 // Site's connection was delegated.
13234 continue;
13235 }
13236
13237 if ( $fs->is_anonymous_site( $blog_id ) ) {
13238 // Site connection was already skipped.
13239 continue;
13240 }
13241
13242 $pending_sites[] = self::get_site_info( $site );
13243 }
13244
13245 if ( ! empty( $pending_sites ) ) {
13246 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
13247 $fs->skip_connection( $pending_sites );
13248 } else {
13249 $fs->delegate_connection( $pending_sites );
13250 }
13251 }
13252 }
13253 }
13254 }
13255
13256 if ( false === $error && true === $fs->_storage->require_license_activation ) {
13257 $fs->_storage->require_license_activation = false;
13258 }
13259
13260 $result = array(
13261 'success' => ( false === $error )
13262 );
13263
13264 if ( false !== $error ) {
13265 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
13266 } else {
13267 if ( $fs->is_addon() || $fs->has_addons() ) {
13268 /**
13269 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
13270 * an updated valid user licenses collection will be fetched from the server which is used to also
13271 * update the account add-ons (add-ons the user has licenses for).
13272 *
13273 * @author Leo Fajardo (@leorw)
13274 * @since 2.2.4
13275 */
13276 $fs->purge_valid_user_licenses_cache();
13277 }
13278
13279 $result['next_page'] = $next_page;
13280 }
13281
13282 return $result;
13283 }
13284
13285 /**
13286 * @author Leo Fajardo (@leorw)
13287 * @since 2.3.2
13288 *
13289 * @return array {
13290 * @key string Product slug.
13291 * @value array {
13292 * @property FS_Site $site
13293 * @property FS_Plugin_License $license
13294 * }
13295 * }
13296 */
13297 private function get_parent_and_addons_installs_info() {
13298 $fs = $this->is_addon() ?
13299 $this->get_parent_instance() :
13300 $this;
13301
13302 $installed_addons_ids = array();
13303
13304 $installed_addons_instances = $fs->get_installed_addons();
13305 foreach ( $installed_addons_instances as $instance ) {
13306 $installed_addons_ids[] = $instance->get_id();
13307 }
13308
13309 $addons_ids = array_unique( array_merge(
13310 $installed_addons_ids,
13311 $fs->get_updated_account_addons()
13312 ) );
13313
13314 // Add parent product info.
13315 $installs_info_by_slug_map = array(
13316 $fs->get_slug() => array(
13317 'install' => $fs->get_site(),
13318 'license' => $fs->_get_license()
13319 )
13320 );
13321
13322 foreach ( $addons_ids as $addon_id ) {
13323 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
13324
13325 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
13326
13327 if ( ! $addon_info['is_connected'] ) {
13328 // Add-on is not associated with an install entity.
13329 continue;
13330 }
13331
13332 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
13333 'install' => $addon_info['site'],
13334 'license' => isset( $addon_info['license'] ) ?
13335 $addon_info['license'] :
13336 null
13337 );
13338 }
13339
13340 return $installs_info_by_slug_map;
13341 }
13342
13343 /**
13344 * @author Leo Fajardo (@leorw)
13345 * @since 1.2.3.1
13346 */
13347 function _network_activate_ajax_action() {
13348 $this->_logger->entrance();
13349
13350 $this->check_ajax_referer( 'network_activate' );
13351
13352 $plugin_id = fs_request_get( 'module_id', '', 'post' );
13353 $fs = ( $plugin_id == $this->_module_id ) ?
13354 $this :
13355 $this->get_addon_instance( $plugin_id );
13356
13357 $error = false;
13358
13359 $sites = fs_request_get( 'sites', array(), 'post' );
13360 if ( is_array( $sites ) && ! empty( $sites ) ) {
13361 $sites_by_action = array(
13362 'allow' => array(),
13363 'delegate' => array(),
13364 'skip' => array()
13365 );
13366
13367 foreach ( $sites as $site ) {
13368 $sites_by_action[ $site['action'] ][] = $site;
13369 }
13370
13371 $total_sites = count( $sites );
13372 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
13373
13374 $next_page = '';
13375
13376 $has_any_install = fs_request_get_bool( 'has_any_install' );
13377
13378 if ( $total_sites === $total_sites_to_delegate &&
13379 ! $this->is_network_upgrade_mode() &&
13380 ! $has_any_install
13381 ) {
13382 $this->delegate_connection();
13383 } else {
13384 if ( ! empty( $sites_by_action['delegate'] ) ) {
13385 $this->delegate_connection( $sites_by_action['delegate'] );
13386 }
13387
13388 if ( ! empty( $sites_by_action['skip'] ) ) {
13389 $this->skip_connection( $sites_by_action['skip'] );
13390 }
13391
13392 if ( empty( $sites_by_action['allow'] ) ) {
13393 if ( $has_any_install ) {
13394 $first_install = $fs->find_first_install();
13395
13396 if ( ! is_null( $first_install ) ) {
13397 $fs->_site = $first_install['install'];
13398 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
13399
13400 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
13401 $fs->_storage->network_user_id = $fs->_user->id;
13402 }
13403 }
13404 } else {
13405 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
13406 $next_page = $fs->opt_in(
13407 false,
13408 false,
13409 false,
13410 false,
13411 false,
13412 false,
13413 false,
13414 fs_request_get_bool( 'is_marketing_allowed', null ),
13415 $sites_by_action['allow']
13416 );
13417 } else {
13418 $next_page = $fs->install_with_user(
13419 $this->get_network_user(),
13420 false,
13421 false,
13422 false,
13423 true,
13424 $sites_by_action['allow']
13425 );
13426 }
13427
13428 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
13429 $error = $next_page->error;
13430 }
13431 }
13432 }
13433
13434 if ( empty( $next_page ) ) {
13435 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
13436 }
13437 } else {
13438 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
13439 }
13440
13441 $result = array(
13442 'success' => ( false === $error )
13443 );
13444
13445 if ( false !== $error ) {
13446 $result['error'] = $error;
13447 } else {
13448 $result['next_page'] = $next_page;
13449 }
13450
13451 echo json_encode( $result );
13452
13453 exit;
13454 }
13455
13456 /**
13457 * Billing update AJAX callback.
13458 *
13459 * @author Vova Feldman (@svovaf)
13460 * @since 1.2.1.5
13461 */
13462 function _update_billing_ajax_action() {
13463 $this->_logger->entrance();
13464
13465 $this->check_ajax_referer( 'update_billing' );
13466
13467 if ( ! $this->is_user_admin() ) {
13468 // Only for admins.
13469 self::shoot_ajax_failure();
13470 }
13471
13472 $billing = fs_request_get( 'billing' );
13473
13474 $api = $this->get_api_user_scope();
13475 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
13476 'plugin_id' => $this->get_parent_id(),
13477 ) ) );
13478
13479 if ( ! $this->is_api_result_entity( $result ) ) {
13480 self::shoot_ajax_failure();
13481 }
13482
13483 // Purge cached billing.
13484 $this->get_api_user_scope()->purge_cache( 'billing.json' );
13485
13486 self::shoot_ajax_success();
13487 }
13488
13489 /**
13490 * Trial start for anonymous users (AJAX callback).
13491 *
13492 * @author Vova Feldman (@svovaf)
13493 * @since 1.2.1.5
13494 */
13495 function _start_trial_ajax_action() {
13496 $this->_logger->entrance();
13497
13498 $this->check_ajax_referer( 'start_trial' );
13499
13500 if ( ! $this->is_user_admin() ) {
13501 // Only for admins.
13502 self::shoot_ajax_failure();
13503 }
13504
13505 $trial_data = fs_request_get( 'trial' );
13506
13507 $next_page = $this->opt_in(
13508 false,
13509 false,
13510 false,
13511 false,
13512 false,
13513 $trial_data['plan_id']
13514 );
13515
13516 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
13517 self::shoot_ajax_failure(
13518 isset( $next_page->error ) ?
13519 $next_page->error->message :
13520 var_export( $next_page, true )
13521 );
13522 }
13523
13524 $this->shoot_ajax_success( array(
13525 'next_page' => $next_page,
13526 ) );
13527 }
13528
13529 /**
13530 * @author Leo Fajardo (@leorw)
13531 * @since 1.2.0
13532 */
13533 function _resend_license_key_ajax_action() {
13534 $this->_logger->entrance();
13535
13536 $this->check_ajax_referer( 'resend_license_key' );
13537
13538 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
13539
13540 if ( empty( $email_address ) ) {
13541 exit;
13542 }
13543
13544 $error = false;
13545
13546 $api = $this->get_api_plugin_scope();
13547 $result = $api->call( '/licenses/resend.json', 'post',
13548 array(
13549 'email' => $email_address,
13550 'url' => home_url(),
13551 )
13552 );
13553
13554 if ( is_object( $result ) && isset( $result->error ) ) {
13555 $error = $result->error;
13556
13557 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
13558 $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' );
13559 } else if ( 'no_license' === $error->code ) {
13560 $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' );
13561 } else {
13562 $error = $error->message;
13563 }
13564 }
13565
13566 $licenses = array(
13567 'success' => ( false === $error )
13568 );
13569
13570 if ( false !== $error ) {
13571 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
13572 }
13573
13574 echo json_encode( $licenses );
13575
13576 exit;
13577 }
13578
13579 /**
13580 * @author Vova Feldman (@svovaf)
13581 * @since 1.2.1.8
13582 *
13583 * @var string
13584 */
13585 private static $_pagenow;
13586
13587 /**
13588 * Get current page or the referer if executing a WP AJAX request.
13589 *
13590 * @author Vova Feldman (@svovaf)
13591 * @since 1.2.1.8
13592 *
13593 * @return string
13594 */
13595 static function get_current_page() {
13596 if ( ! isset( self::$_pagenow ) ) {
13597 global $pagenow;
13598 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
13599 /**
13600 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
13601 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
13602 *
13603 * @author Leo Fajardo (@leorw)
13604 * @since 2.2.3
13605 */
13606 if ( is_network_admin() ) {
13607 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
13608 } else if ( is_user_admin() ) {
13609 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
13610 } else {
13611 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
13612 }
13613
13614 $pagenow = $self_matches[1];
13615 $pagenow = trim( $pagenow, '/' );
13616 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
13617 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
13618 $pagenow = 'index.php';
13619 } else {
13620 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
13621 $pagenow = strtolower( $self_matches[1] );
13622 if ( '.php' !== substr($pagenow, -4, 4) )
13623 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
13624 }
13625 }
13626
13627 self::$_pagenow = $pagenow;
13628
13629 if ( self::is_ajax() &&
13630 'admin-ajax.php' === $pagenow
13631 ) {
13632 $referer = fs_get_raw_referer();
13633
13634 if ( is_string( $referer ) ) {
13635 $parts = explode( '?', $referer );
13636
13637 self::$_pagenow = basename( $parts[0] );
13638 }
13639 }
13640 }
13641
13642 return self::$_pagenow;
13643 }
13644
13645 /**
13646 * Helper method to check if user in the plugins page.
13647 *
13648 * @author Vova Feldman (@svovaf)
13649 * @since 1.2.1.5
13650 *
13651 * @return bool
13652 */
13653 static function is_plugins_page() {
13654 return ( 'plugins.php' === self::get_current_page() );
13655 }
13656
13657 /**
13658 * @author Leo Fajardo (@leorw)
13659 * @since 2.2.3
13660 *
13661 * @return bool
13662 */
13663 static function is_plugin_install_page() {
13664 return ( 'plugin-install.php' === self::get_current_page() );
13665 }
13666
13667 /**
13668 * @author Leo Fajardo (@leorw)
13669 * @since 2.0.2
13670 *
13671 * @return bool
13672 */
13673 static function is_updates_page() {
13674 return ( 'update-core.php' === self::get_current_page() );
13675 }
13676
13677 /**
13678 * Helper method to check if user in the themes page.
13679 *
13680 * @author Vova Feldman (@svovaf)
13681 * @since 1.2.2.6
13682 *
13683 * @return bool
13684 */
13685 static function is_themes_page() {
13686 return ( 'themes.php' === self::get_current_page() );
13687 }
13688
13689 #----------------------------------------------------------------------------------
13690 #region Affiliation
13691 #----------------------------------------------------------------------------------
13692
13693 /**
13694 * @author Leo Fajardo (@leorw)
13695 * @since 1.2.3
13696 *
13697 * @return bool
13698 */
13699 function has_affiliate_program() {
13700 if ( ! is_object( $this->_plugin ) ) {
13701 return false;
13702 }
13703
13704 return $this->_plugin->has_affiliate_program();
13705 }
13706
13707 /**
13708 * @author Leo Fajardo (@leorw)
13709 * @since 1.2.4
13710 */
13711 private function fetch_affiliate_terms() {
13712 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
13713 $plugins_api = $this->get_api_plugin_scope();
13714 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
13715
13716 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
13717 return;
13718 }
13719
13720 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
13721 }
13722 }
13723
13724 /**
13725 * @author Leo Fajardo (@leorw)
13726 * @since 1.2.4
13727 */
13728 private function fetch_affiliate_and_custom_terms() {
13729 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
13730 $application_data = $this->_storage->affiliate_application_data;
13731 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
13732
13733 $users_api = $this->get_api_user_scope();
13734 $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
13735 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
13736 if ( ! empty( $result->affiliates ) ) {
13737 $affiliate = new FS_Affiliate( $result->affiliates[0] );
13738
13739 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
13740 $application_data['status'] = $affiliate->status;
13741 $this->_storage->affiliate_application_data = $application_data;
13742 }
13743
13744 if ( $affiliate->is_using_custom_terms ) {
13745 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
13746 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
13747 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
13748 }
13749 }
13750
13751 $this->affiliate = $affiliate;
13752 }
13753 }
13754 }
13755 }
13756
13757 /**
13758 * @author Leo Fajardo (@leorw)
13759 * @since 1.2.3
13760 */
13761 private function fetch_affiliate_and_terms() {
13762 $this->_logger->entrance();
13763
13764 $this->fetch_affiliate_terms();
13765 $this->fetch_affiliate_and_custom_terms();
13766 }
13767
13768 /**
13769 * @author Leo Fajardo (@leorw)
13770 * @since 1.2.3
13771 *
13772 * @return FS_Affiliate
13773 */
13774 function get_affiliate() {
13775 return $this->affiliate;
13776 }
13777
13778
13779 /**
13780 * @author Leo Fajardo (@leorw)
13781 * @since 1.2.3
13782 *
13783 * @return FS_AffiliateTerms
13784 */
13785 function get_affiliate_terms() {
13786 return is_object( $this->custom_affiliate_terms ) ?
13787 $this->custom_affiliate_terms :
13788 $this->plugin_affiliate_terms;
13789 }
13790
13791 /**
13792 * @author Leo Fajardo (@leorw)
13793 * @since 1.2.3
13794 */
13795 function _submit_affiliate_application() {
13796 $this->_logger->entrance();
13797
13798 $this->check_ajax_referer( 'submit_affiliate_application' );
13799
13800 if ( ! $this->is_user_admin() ) {
13801 // Only for admins.
13802 self::shoot_ajax_failure();
13803 }
13804
13805 $affiliate = fs_request_get( 'affiliate' );
13806
13807 if ( empty( $affiliate['promotion_methods'] ) ) {
13808 unset( $affiliate['promotion_methods'] );
13809 }
13810
13811 if ( ! empty( $affiliate['additional_domains'] ) ) {
13812 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
13813 }
13814
13815 if ( ! $this->is_registered() ) {
13816 // Opt in but don't track usage.
13817 $next_page = $this->opt_in(
13818 false,
13819 false,
13820 false,
13821 false,
13822 false,
13823 false,
13824 true
13825 );
13826
13827 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
13828 self::shoot_ajax_failure(
13829 isset( $next_page->error ) ?
13830 $next_page->error->message :
13831 var_export( $next_page, true )
13832 );
13833 } else if ( $this->is_pending_activation() ) {
13834 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) );
13835 }
13836 }
13837
13838 $this->fetch_affiliate_terms();
13839
13840 $api = $this->get_api_user_scope();
13841 $result = $api->call(
13842 ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
13843 'post',
13844 $affiliate
13845 );
13846
13847 if ( $this->is_api_error( $result ) ) {
13848 self::shoot_ajax_failure(
13849 isset( $result->error ) ?
13850 $result->error->message :
13851 var_export( $result, true )
13852 );
13853 } else {
13854 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
13855 $this->_admin_notices->remove_sticky( 'affiliate_program' );
13856 }
13857
13858 $affiliate_application_data = array(
13859 'status' => 'pending',
13860 'stats_description' => $affiliate['stats_description'],
13861 'promotion_method_description' => $affiliate['promotion_method_description'],
13862 );
13863
13864 if ( ! empty( $affiliate['promotion_methods'] ) ) {
13865 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
13866 }
13867
13868 if ( ! empty( $affiliate['domain'] ) ) {
13869 $affiliate_application_data['domain'] = $affiliate['domain'];
13870 }
13871
13872 if ( ! empty( $affiliate['additional_domains'] ) ) {
13873 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
13874 }
13875
13876 $this->_storage->affiliate_application_data = $affiliate_application_data;
13877 }
13878
13879 // Purge cached affiliate.
13880 $api->purge_cache( 'affiliate.json' );
13881
13882 self::shoot_ajax_success( $result );
13883 }
13884
13885 /**
13886 * @author Leo Fajardo (@leorw)
13887 * @since 1.2.3
13888 *
13889 * @return array|null
13890 */
13891 function get_affiliate_application_data() {
13892 if ( empty( $this->_storage->affiliate_application_data ) ) {
13893 return null;
13894 }
13895
13896 return $this->_storage->affiliate_application_data;
13897 }
13898
13899 #endregion Affiliation ------------------------------------------------------------
13900
13901 #----------------------------------------------------------------------------------
13902 #region URL Generators
13903 #----------------------------------------------------------------------------------
13904
13905 /**
13906 * Alias to pricing_url().
13907 *
13908 * @author Vova Feldman (@svovaf)
13909 * @since 1.0.2
13910 *
13911 * @uses pricing_url()
13912 *
13913 * @param string $period Billing cycle
13914 * @param bool $is_trial
13915 *
13916 * @return string
13917 */
13918 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
13919 return $this->pricing_url( $period, $is_trial );
13920 }
13921
13922 /**
13923 * @author Vova Feldman (@svovaf)
13924 * @since 1.0.9
13925 *
13926 * @uses get_upgrade_url()
13927 *
13928 * @return string
13929 */
13930 function get_trial_url() {
13931 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
13932 }
13933
13934 /**
13935 * @author Leo Fajardo (@leorw)
13936 * @since 2.1.4
13937 *
13938 * @param string $new_version
13939 *
13940 * @return string
13941 */
13942 function version_upgrade_checkout_link( $new_version ) {
13943 if ( ! is_object( $this->_license ) ) {
13944 $url = $this->pricing_url();
13945
13946 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
13947 } else {
13948 $subscription = $this->_get_subscription( $this->_license->id );
13949
13950 $url = $this->checkout_url(
13951 is_object( $subscription ) ?
13952 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
13953 WP_FS__PERIOD_LIFETIME,
13954 false,
13955 array( 'licenses' => $this->_license->quota )
13956 );
13957
13958 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
13959 }
13960
13961 return sprintf(
13962 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
13963 sprintf( '<a href="%s">%s</a>', $url, $purchase_license_text ),
13964 $new_version
13965 );
13966 }
13967
13968 /**
13969 * Plugin's pricing URL.
13970 *
13971 * @author Vova Feldman (@svovaf)
13972 * @since 1.0.4
13973 *
13974 * @param string $billing_cycle Billing cycle
13975 *
13976 * @param bool $is_trial
13977 *
13978 * @return string
13979 */
13980 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
13981 $this->_logger->entrance();
13982
13983 $params = array(
13984 'billing_cycle' => $billing_cycle
13985 );
13986
13987 if ( $is_trial ) {
13988 $params['trial'] = 'true';
13989 }
13990
13991 $url = $this->is_addon() ?
13992 $this->_parent->addon_url( $this->_slug ) :
13993 $this->_get_admin_page_url( 'pricing', $params );
13994
13995 return $this->apply_filters( 'pricing_url', $url );
13996 }
13997
13998 /**
13999 * Checkout page URL.
14000 *
14001 * @author Vova Feldman (@svovaf)
14002 * @since 1.0.6
14003 *
14004 * @param string $billing_cycle Billing cycle
14005 * @param bool $is_trial
14006 * @param array $extra (optional) Extra parameters, override other query params.
14007 * @param bool|null $network
14008 *
14009 * @return string
14010 */
14011 function checkout_url(
14012 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14013 $is_trial = false,
14014 $extra = array(),
14015 $network = null
14016 ) {
14017 $this->_logger->entrance();
14018
14019 $params = array(
14020 'checkout' => 'true',
14021 'billing_cycle' => $billing_cycle,
14022 );
14023
14024 if ( $is_trial ) {
14025 $params['trial'] = 'true';
14026 }
14027
14028 /**
14029 * Params in extra override other params.
14030 */
14031 $params = array_merge( $params, $extra );
14032
14033 return $this->_get_admin_page_url( 'pricing', $params, $network );
14034 }
14035
14036 /**
14037 * Add-on checkout URL.
14038 *
14039 * @author Vova Feldman (@svovaf)
14040 * @since 1.1.7
14041 *
14042 * @param number $addon_id
14043 * @param number $pricing_id
14044 * @param string $billing_cycle
14045 * @param bool $is_trial
14046 * @param bool|null $network
14047 *
14048 * @return string
14049 */
14050 function addon_checkout_url(
14051 $addon_id,
14052 $pricing_id,
14053 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14054 $is_trial = false,
14055 $network = null
14056 ) {
14057 return $this->checkout_url( $billing_cycle, $is_trial, array(
14058 'plugin_id' => $addon_id,
14059 'pricing_id' => $pricing_id,
14060 ), $network );
14061 }
14062
14063 #endregion
14064
14065 #endregion ------------------------------------------------------------------
14066
14067 /**
14068 * Check if plugin has any add-ons.
14069 *
14070 * @author Vova Feldman (@svovaf)
14071 * @since 1.0.5
14072 *
14073 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14074 *
14075 * @return bool
14076 */
14077 function has_addons() {
14078 $this->_logger->entrance();
14079
14080 return $this->_has_addons;
14081 }
14082
14083 /**
14084 * Check if plugin can work in anonymous mode.
14085 *
14086 * @author Vova Feldman (@svovaf)
14087 * @since 1.0.9
14088 *
14089 * @return bool
14090 *
14091 * @deprecated Please use is_enable_anonymous() instead.
14092 */
14093 function enable_anonymous() {
14094 return $this->_enable_anonymous;
14095 }
14096
14097 /**
14098 * Check if plugin can work in anonymous mode.
14099 *
14100 * @author Vova Feldman (@svovaf)
14101 * @since 1.1.9
14102 *
14103 * @return bool
14104 */
14105 function is_enable_anonymous() {
14106 return $this->_enable_anonymous;
14107 }
14108
14109 /**
14110 * Check if plugin is premium only (no free plans).
14111 *
14112 * @author Vova Feldman (@svovaf)
14113 * @since 1.1.9
14114 *
14115 * @return bool
14116 */
14117 function is_only_premium() {
14118 return $this->_is_premium_only;
14119 }
14120
14121 /**
14122 * Checks if the plugin's type is "plugin". The other type is "theme".
14123 *
14124 * @author Leo Fajardo (@leorw)
14125 * @since 1.2.2
14126 *
14127 * @return bool
14128 */
14129 function is_plugin() {
14130 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14131 }
14132
14133 /**
14134 * @author Leo Fajardo (@leorw)
14135 * @since 1.2.2
14136 *
14137 * @return string
14138 */
14139 function get_module_type() {
14140 if ( ! isset( $this->_module_type ) ) {
14141 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14142 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14143 }
14144
14145 return $this->_module_type;
14146 }
14147
14148 /**
14149 * @author Leo Fajardo (@leorw)
14150 * @since 1.2.2
14151 *
14152 * @return string
14153 */
14154 function get_plugin_main_file_path() {
14155 return $this->_plugin_main_file_path;
14156 }
14157
14158 /**
14159 * Check if module has a premium code version.
14160 *
14161 * Serviceware module might be freemium without any
14162 * premium code version, where the paid features
14163 * are all part of the service.
14164 *
14165 * @author Vova Feldman (@svovaf)
14166 * @since 1.2.1.6
14167 *
14168 * @return bool
14169 */
14170 function has_premium_version() {
14171 return $this->_has_premium_version;
14172 }
14173
14174 /**
14175 * Check if feature supported with current site's plan.
14176 *
14177 * @author Vova Feldman (@svovaf)
14178 * @since 1.0.1
14179 *
14180 * @todo IMPLEMENT
14181 *
14182 * @param number $feature_id
14183 *
14184 * @throws Exception
14185 */
14186 function is_feature_supported( $feature_id ) {
14187 throw new Exception( 'not implemented' );
14188 }
14189
14190 /**
14191 * @author Vova Feldman (@svovaf)
14192 * @since 1.0.1
14193 *
14194 * @return bool Is running in SSL/HTTPS
14195 */
14196 function is_ssl() {
14197 return WP_FS__IS_HTTPS;
14198 }
14199
14200 /**
14201 * @author Vova Feldman (@svovaf)
14202 * @since 1.0.9
14203 *
14204 * @return bool Is running in AJAX call.
14205 *
14206 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
14207 */
14208 static function is_ajax() {
14209 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
14210 }
14211
14212 /**
14213 * Check if it's an AJAX call targeted for the current module.
14214 *
14215 * @author Vova Feldman (@svovaf)
14216 * @since 1.2.0
14217 *
14218 * @param array|string $actions Collection of AJAX actions.
14219 *
14220 * @return bool
14221 */
14222 function is_ajax_action( $actions ) {
14223 // Verify it's an ajax call.
14224 if ( ! self::is_ajax() ) {
14225 return false;
14226 }
14227
14228 // Verify the call is relevant for the plugin.
14229 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
14230 return false;
14231 }
14232
14233 // Verify it's one of the specified actions.
14234 if ( is_string( $actions ) ) {
14235 $actions = explode( ',', $actions );
14236 }
14237
14238 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14239 $ajax_action = fs_request_get( 'action' );
14240
14241 foreach ( $actions as $action ) {
14242 if ( $ajax_action === $this->get_action_tag( $action ) ) {
14243 return true;
14244 }
14245 }
14246 }
14247
14248 return false;
14249 }
14250
14251 /**
14252 * Check if it's an AJAX call targeted for current request.
14253 *
14254 * @author Vova Feldman (@svovaf)
14255 * @since 1.2.0
14256 *
14257 * @param array|string $actions Collection of AJAX actions.
14258 * @param number|null $module_id
14259 *
14260 * @return bool
14261 */
14262 static function is_ajax_action_static( $actions, $module_id = null ) {
14263 // Verify it's an ajax call.
14264 if ( ! self::is_ajax() ) {
14265 return false;
14266 }
14267
14268
14269 if ( ! empty( $module_id ) ) {
14270 // Verify the call is relevant for the plugin.
14271 if ( $module_id != fs_request_get( 'module_id' ) ) {
14272 return false;
14273 }
14274 }
14275
14276 // Verify it's one of the specified actions.
14277 if ( is_string( $actions ) ) {
14278 $actions = explode( ',', $actions );
14279 }
14280
14281 if ( is_array( $actions ) && 0 < count( $actions ) ) {
14282 $ajax_action = fs_request_get( 'action' );
14283
14284 foreach ( $actions as $action ) {
14285 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
14286 return true;
14287 }
14288 }
14289 }
14290
14291 return false;
14292 }
14293
14294 /**
14295 * @author Vova Feldman (@svovaf)
14296 * @since 1.1.7
14297 *
14298 * @return bool
14299 */
14300 static function is_cron() {
14301 return ( defined( 'DOING_CRON' ) && DOING_CRON );
14302 }
14303
14304 /**
14305 * Check if a real user is visiting the admin dashboard.
14306 *
14307 * @author Vova Feldman (@svovaf)
14308 * @since 1.1.7
14309 *
14310 * @return bool
14311 */
14312 function is_user_in_admin() {
14313 return is_admin() && ! self::is_ajax() && ! self::is_cron();
14314 }
14315
14316 /**
14317 * Check if a real user is in the customizer view.
14318 *
14319 * @author Vova Feldman (@svovaf)
14320 * @since 1.2.2.7
14321 *
14322 * @return bool
14323 */
14324 static function is_customizer() {
14325 return is_customize_preview();
14326 }
14327
14328 /**
14329 * Check if running in HTTPS and if site's plan matching the specified plan.
14330 *
14331 * @param string $plan
14332 * @param bool $exact
14333 *
14334 * @return bool
14335 */
14336 function is_ssl_and_plan( $plan, $exact = false ) {
14337 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
14338 }
14339
14340 /**
14341 * Construct plugin's settings page URL.
14342 *
14343 * @author Vova Feldman (@svovaf)
14344 * @since 1.0.4
14345 *
14346 * @param string $page
14347 * @param array $params
14348 * @param bool|null $network
14349 *
14350 * @return string
14351 */
14352 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
14353 if ( is_null( $network ) ) {
14354 $network = (
14355 $this->_is_network_active &&
14356 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
14357 );
14358 }
14359
14360 if ( 0 < count( $params ) ) {
14361 foreach ( $params as $k => $v ) {
14362 $params[ $k ] = urlencode( $v );
14363 }
14364 }
14365
14366 $page_param = $this->_menu->get_slug( $page );
14367
14368 if ( empty( $page ) &&
14369 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
14370 $this->show_opt_in_on_themes_page()
14371 ) {
14372 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
14373
14374 return add_query_arg(
14375 $params,
14376 $this->admin_url( 'themes.php', 'admin', $network )
14377 );
14378 }
14379
14380 if ( ! $this->has_settings_menu() ) {
14381 if ( ! empty( $page ) ) {
14382 // Module doesn't have a setting page, but since the request is for
14383 // a specific Freemius page, use the admin.php path.
14384 return add_query_arg( array_merge( $params, array(
14385 'page' => $page_param,
14386 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14387 } else {
14388 if ( $this->is_activation_mode() ) {
14389 /**
14390 * @author Vova Feldman
14391 * @since 1.2.1.6
14392 *
14393 * If plugin doesn't have a settings page, create one for the opt-in screen.
14394 */
14395 return add_query_arg( array_merge( $params, array(
14396 'page' => $this->_slug,
14397 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14398 } else {
14399 // Plugin without a settings page.
14400 return add_query_arg(
14401 $params,
14402 $this->admin_url( 'plugins.php', 'admin', $network )
14403 );
14404 }
14405 }
14406 }
14407
14408 // Module has a submenu settings page.
14409 if ( ! $this->_menu->is_top_level() ) {
14410 $parent_slug = $this->_menu->get_parent_slug();
14411 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
14412 $parent_slug :
14413 'admin.php';
14414
14415 return add_query_arg( array_merge( $params, array(
14416 'page' => $page_param,
14417 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
14418 }
14419
14420 // Module has a top level CPT settings page.
14421 if ( $this->_menu->is_cpt() ) {
14422 if ( empty( $page ) && $this->is_activation_mode() ) {
14423 return add_query_arg( array_merge( $params, array(
14424 'page' => $page_param
14425 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14426 } else {
14427 if ( ! empty( $page ) ) {
14428 $params['page'] = $page_param;
14429 }
14430
14431 return add_query_arg(
14432 $params,
14433 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
14434 );
14435 }
14436 }
14437
14438 // Module has a custom top level settings page.
14439 return add_query_arg( array_merge( $params, array(
14440 'page' => $page_param,
14441 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
14442 }
14443
14444 #--------------------------------------------------------------------------------
14445 #region Multisite
14446 #--------------------------------------------------------------------------------
14447
14448 /**
14449 * @author Leo Fajardo (@leorw)
14450 * @since 2.0.0
14451 *
14452 * @return bool
14453 */
14454 function is_network_active() {
14455 return $this->_is_network_active;
14456 }
14457
14458 /**
14459 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
14460 *
14461 * @author Leo Fajardo (@leorw)
14462 * @since 2.0.0
14463 *
14464 * @param array|null $sites
14465 */
14466 private function delegate_connection( $sites = null ) {
14467 $this->_logger->entrance();
14468
14469 $this->_admin_notices->remove_sticky( 'connect_account' );
14470
14471 if ( is_null( $sites ) ) {
14472 // All sites delegation.
14473 $this->_storage->store( 'is_delegated_connection', true, true, true );
14474 } else {
14475 // Specified sites delegation.
14476 foreach ( $sites as $site ) {
14477 $this->delegate_site_connection( $site['blog_id'] );
14478 }
14479 }
14480
14481 $this->network_upgrade_mode_completed();
14482 }
14483
14484 /**
14485 * Delegate specific network site conncetion to the site admin.
14486 *
14487 * @author Vova Feldman (@svovaf)
14488 * @since 2.0.0
14489 *
14490 * @param int $blog_id
14491 */
14492 private function delegate_site_connection( $blog_id ) {
14493 $this->_storage->store( 'is_delegated_connection', true, $blog_id, true );
14494 }
14495
14496 /**
14497 * Check if super-admin delegated the connection of ALL sites to the site admins.
14498 *
14499 * @author Vova Feldman (@svovaf)
14500 * @since 2.0.0
14501 *
14502 * @return bool
14503 */
14504 function is_network_delegated_connection() {
14505 if ( ! $this->_is_network_active ) {
14506 return false;
14507 }
14508
14509 return $this->_storage->get( 'is_delegated_connection', false, true );
14510 }
14511
14512 /**
14513 * @author Leo Fajardo (@leorw)
14514 * @since 2.0.0
14515 *
14516 * @param int $blog_id
14517 *
14518 * @return bool
14519 */
14520 function is_site_delegated_connection( $blog_id = 0 ) {
14521 if ( ! $this->_is_network_active ) {
14522 return false;
14523 }
14524
14525 if ( 0 == $blog_id ) {
14526 $blog_id = get_current_blog_id();
14527 }
14528
14529 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
14530 }
14531
14532 /**
14533 * Check if delegated the connection. When running within the the network admin,
14534 * and haven't specified the blog ID, checks if network level delegated. If running
14535 * within a site admin or specified a blog ID, check if delegated the connection for
14536 * the current context site.
14537 *
14538 * If executed outside the the admin, check if delegated the connection
14539 * for the current context site OR the whole network.
14540 *
14541 * @author Vova Feldman (@svovaf)
14542 * @since 2.0.0
14543 *
14544 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
14545 *
14546 * @return bool
14547 */
14548 function is_delegated_connection( $blog_id = 0 ) {
14549 if ( ! $this->_is_network_active ) {
14550 return false;
14551 }
14552
14553 if ( fs_is_network_admin() && 0 == $blog_id ) {
14554 return $this->is_network_delegated_connection();
14555 }
14556
14557 return (
14558 $this->is_network_delegated_connection() ||
14559 $this->is_site_delegated_connection( $blog_id )
14560 );
14561 }
14562
14563 /**
14564 * Check if the current module is active for the site.
14565 *
14566 * @author Vova Feldman (@svovaf)
14567 * @since 2.0.0
14568 *
14569 * @param int $blog_id
14570 *
14571 * @return bool
14572 */
14573 function is_active_for_site( $blog_id ) {
14574 if ( ! is_multisite() ) {
14575 // Not a multisite and this code is executed, means that the plugin is active.
14576 return true;
14577 }
14578
14579 if ( $this->is_theme() ) {
14580 // All themes are site level activated.
14581 return true;
14582 }
14583
14584 if ( $this->_is_network_active ) {
14585 // Plugin was network activated so it's active.
14586 return true;
14587 }
14588
14589 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
14590 }
14591
14592 /**
14593 * @author Leo Fajardo (@leorw)
14594 * @since 2.0.0
14595 *
14596 * @return array Active & public sites collection.
14597 */
14598 static function get_sites() {
14599 if ( ! is_multisite() ) {
14600 return array();
14601 }
14602
14603 /**
14604 * For consistency with get_blog_list() which only return active public sites.
14605 *
14606 * @author Vova Feldman (@svovaf)
14607 */
14608 $args = array(
14609 /**
14610 * Commented out in order to handle the migration of site options whether the site is public or not.
14611 *
14612 * @author Leo Fajardo (@leorw)
14613 * @since 2.2.1
14614 */
14615 // 'public' => 1,
14616 'archived' => 0,
14617 'mature' => 0,
14618 'spam' => 0,
14619 'deleted' => 0,
14620 );
14621
14622 if ( function_exists( 'get_sites' ) ) {
14623 // For WP 4.6 and above.
14624 return get_sites( $args );
14625 } else if ( function_exists( 'wp_get_sites' ) ) {
14626 // For WP 3.7 to WP 4.5.
14627 return wp_get_sites( $args );
14628 } else {
14629 // For WP 3.6 and below.
14630 return get_blog_list( 0, 'all' );
14631 }
14632 }
14633
14634 /**
14635 * Checks if a given blog is active.
14636 *
14637 * @author Vova Feldman (@svovaf)
14638 * @since 2.0.0
14639 *
14640 * @param $blog_id
14641 *
14642 * @return bool
14643 */
14644 private static function is_site_active( $blog_id ) {
14645 global $wpdb;
14646
14647 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
14648
14649 if ( ! is_object( $blog_info ) ) {
14650 return false;
14651 }
14652
14653 return (
14654 true == $blog_info->public &&
14655 false == $blog_info->archived &&
14656 false == $blog_info->mature &&
14657 false == $blog_info->spam &&
14658 false == $blog_info->deleted
14659 );
14660 }
14661
14662 /**
14663 * Get a mapping between the site addresses to their blog IDs.
14664 *
14665 * @author Vova Feldman (@svovaf)
14666 * @since 2.0.0
14667 *
14668 * @return array {
14669 * @key string Site address without protocol with a trailing slash.
14670 * @value int Site's blog ID.
14671 * }
14672 */
14673 private function get_address_to_blog_map() {
14674 $sites = self::get_sites();
14675
14676 // Map site addresses to their blog IDs.
14677 $address_to_blog_map = array();
14678 foreach ( $sites as $site ) {
14679 $blog_id = self::get_site_blog_id( $site );
14680 $address = trailingslashit( fs_strip_url_protocol( get_site_url( $blog_id ) ) );
14681 $address_to_blog_map[ $address ] = $blog_id;
14682 }
14683
14684 return $address_to_blog_map;
14685 }
14686
14687 /**
14688 * Get a mapping between the site addresses to their blog IDs.
14689 *
14690 * @author Vova Feldman (@svovaf)
14691 * @since 2.0.0
14692 *
14693 * @return array {
14694 * @key int Site's blog ID.
14695 * @value FS_Site Associated install.
14696 * }
14697 */
14698 function get_blog_install_map() {
14699 $sites = self::get_sites();
14700
14701 // Map site blog ID to its install.
14702 $install_map = array();
14703
14704 foreach ( $sites as $site ) {
14705 $blog_id = self::get_site_blog_id( $site );
14706 $install = $this->get_install_by_blog_id( $blog_id );
14707
14708 if ( is_object( $install ) ) {
14709 $install_map[ $blog_id ] = $install;
14710 }
14711 }
14712
14713 return $install_map;
14714 }
14715
14716 /**
14717 * Gets a map of module IDs that the given user has opted-in to.
14718 *
14719 * @author Leo Fajardo (@leorw)
14720 * @since 2.1.0
14721 *
14722 * @param number $fs_user_id
14723 *
14724 * @return array {
14725 * @key number $plugin_id
14726 * @value bool Always true.
14727 * }
14728 */
14729 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
14730 self::$_static_logger->entrance();
14731
14732 if ( ! is_multisite() ) {
14733 $installs = array_merge(
14734 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
14735 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
14736 );
14737 } else {
14738 $sites = self::get_sites();
14739
14740 $installs = array();
14741 foreach ( $sites as $site ) {
14742 $blog_id = self::get_site_blog_id( $site );
14743
14744 $installs = array_merge(
14745 $installs,
14746 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
14747 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
14748 );
14749 }
14750 }
14751
14752 $module_ids_map = array();
14753 foreach ( $installs as $install ) {
14754 if ( is_object( $install ) &&
14755 FS_Site::is_valid_id( $install->id ) &&
14756 FS_User::is_valid_id( $install->user_id ) &&
14757 ( $install->user_id == $fs_user_id )
14758 ) {
14759 $module_ids_map[ $install->plugin_id ] = true;
14760 }
14761 }
14762
14763 return $module_ids_map;
14764 }
14765
14766 /**
14767 * @author Leo Fajardo (@leorw)
14768 *
14769 * @return null|array {
14770 * 'install' => FS_Site Module's install,
14771 * 'blog_id' => string The associated blog ID.
14772 * }
14773 */
14774 function find_first_install() {
14775 $sites = self::get_sites();
14776
14777 foreach ( $sites as $site ) {
14778 $blog_id = self::get_site_blog_id( $site );
14779 $install = $this->get_install_by_blog_id( $blog_id );
14780
14781 if ( is_object( $install ) ) {
14782 return array(
14783 'install' => $install,
14784 'blog_id' => $blog_id
14785 );
14786 }
14787 }
14788
14789 return null;
14790 }
14791
14792 /**
14793 * Switches the Freemius site level context to a specified blog.
14794 *
14795 * @author Vova Feldman (@svovaf)
14796 * @since 2.0.0
14797 *
14798 * @param int $blog_id
14799 * @param FS_Site $install
14800 *
14801 * @return bool Since 2.3.1 returns if a switch was made.
14802 */
14803 function switch_to_blog( $blog_id, FS_Site $install = null ) {
14804 if ( $blog_id == $this->_context_is_network_or_blog_id ) {
14805 return false;
14806 }
14807
14808 switch_to_blog( $blog_id );
14809 $this->_context_is_network_or_blog_id = $blog_id;
14810
14811 self::$_accounts->set_site_blog_context( $blog_id );
14812 $this->_storage->set_site_blog_context( $blog_id );
14813 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
14814
14815 $this->_site = is_object( $install ) ?
14816 $install :
14817 $this->get_install_by_blog_id( $blog_id );
14818
14819 $this->_user = false;
14820 $this->_licenses = false;
14821 $this->_license = null;
14822 $this->is_whitelabeled = null;
14823
14824 if ( is_object( $this->_site ) ) {
14825 // Try to fetch user from install.
14826 $this->_user = self::_get_user_by_id( $this->_site->user_id );
14827
14828 if ( ! is_object( $this->_user ) &&
14829 FS_User::is_valid_id( $this->_storage->prev_user_id )
14830 ) {
14831 // Try to fetch previously saved user.
14832 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
14833
14834 if ( ! is_object( $this->_user ) ) {
14835 // Fallback to network's user.
14836 $this->_user = $this->get_network_user();
14837 }
14838 }
14839
14840 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
14841
14842 if ( ! empty( $all_plugin_licenses ) ) {
14843 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
14844 $this->_license = null;
14845 } else {
14846 $license_found = false;
14847 foreach ( $all_plugin_licenses as $license ) {
14848 if ( $license->id == $this->_site->license_id ) {
14849 // License found.
14850 $this->_license = $license;
14851 $license_found = true;
14852 break;
14853 }
14854 }
14855
14856 if ( $license_found ) {
14857 $this->link_license_2_user( $this->_license->id, $this->_user->id );
14858 }
14859 }
14860
14861 $this->_licenses = $this->get_user_licenses( $this->_user->id );
14862 }
14863 }
14864
14865 unset( $this->_site_api );
14866 unset( $this->_user_api );
14867
14868 return false;
14869 }
14870
14871 /**
14872 * Restore the blog context to the blog that originally loaded the module.
14873 *
14874 * @author Vova Feldman (@svovaf)
14875 * @since 2.0.0
14876 */
14877 function restore_current_blog() {
14878 $this->switch_to_blog( $this->_blog_id );
14879 }
14880
14881 /**
14882 * @author Vova Feldman (@svovaf)
14883 * @since 2.0.0
14884 *
14885 * @param array|WP_Site $site
14886 *
14887 * @return int
14888 */
14889 static function get_site_blog_id( &$site ) {
14890 return ( $site instanceof WP_Site ) ?
14891 $site->blog_id :
14892 ( is_object( $site ) && isset( $site->userblog_id ) ?
14893 $site->userblog_id :
14894 $site['blog_id'] );
14895 }
14896
14897 /**
14898 * @author Leo Fajardo (@leorw)
14899 * @since 2.0.0
14900 *
14901 * @param array|WP_Site|null $site
14902 *
14903 * @return array
14904 */
14905 function get_site_info( $site = null ) {
14906 $this->_logger->entrance();
14907
14908 $switched = false;
14909
14910 if ( is_null( $site ) ) {
14911 $url = get_site_url();
14912 $name = get_bloginfo( 'name' );
14913 $blog_id = null;
14914 } else {
14915 $blog_id = self::get_site_blog_id( $site );
14916
14917 if ( get_current_blog_id() != $blog_id ) {
14918 switch_to_blog( $blog_id );
14919 $switched = true;
14920 }
14921
14922 if ( $site instanceof WP_Site ) {
14923 $url = $site->siteurl;
14924 $name = $site->blogname;
14925 } else {
14926 $url = get_site_url( $blog_id );
14927 $name = get_bloginfo( 'name' );
14928 }
14929 }
14930
14931 $info = array(
14932 'uid' => $this->get_anonymous_id( $blog_id ),
14933 'url' => $url,
14934 'title' => $name,
14935 'language' => get_bloginfo( 'language' ),
14936 'charset' => get_bloginfo( 'charset' ),
14937 );
14938
14939 if ( is_numeric( $blog_id ) ) {
14940 $info['blog_id'] = $blog_id;
14941 }
14942
14943 if ( $switched ) {
14944 restore_current_blog();
14945 }
14946
14947 return $info;
14948 }
14949
14950 /**
14951 * Load the module's install based on the blog ID.
14952 *
14953 * @author Vova Feldman (@svovaf)
14954 * @since 2.0.0
14955 *
14956 * @param int|null $blog_id
14957 *
14958 * @return FS_Site
14959 */
14960 function get_install_by_blog_id( $blog_id = null ) {
14961 $installs = self::get_all_sites( $this->_module_type, $blog_id );
14962 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
14963
14964 if ( is_object( $install ) &&
14965 is_numeric( $install->id ) &&
14966 is_numeric( $install->user_id ) &&
14967 FS_Plugin_Plan::is_valid_id( $install->plan_id )
14968 ) {
14969 // Load site.
14970 $install = clone $install;
14971 }
14972
14973 return $install;
14974 }
14975
14976 /**
14977 * Check if module is installed on a specified site.
14978 *
14979 * @author Vova Feldman (@svovaf)
14980 * @since 2.0.0
14981 *
14982 * @param int|null $blog_id
14983 *
14984 * @return bool
14985 */
14986 function is_installed_on_site( $blog_id = null ) {
14987 $installs = self::get_all_sites( $this->_module_type, $blog_id );
14988 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
14989
14990 return (
14991 is_object( $install ) &&
14992 is_numeric( $install->id ) &&
14993 is_numeric( $install->user_id ) &&
14994 FS_Plugin_Plan::is_valid_id( $install->plan_id )
14995 );
14996 }
14997
14998 /**
14999 * Check if super-admin connected at least one site via the network opt-in.
15000 *
15001 * @author Vova Feldman (@svovaf)
15002 * @since 2.0.0
15003 *
15004 * @return bool
15005 */
15006 function is_network_registered() {
15007 if ( ! $this->_is_network_active ) {
15008 return false;
15009 }
15010
15011 return FS_User::is_valid_id( $this->_storage->network_user_id );
15012 }
15013
15014 /**
15015 * Returns the main user associated with the network.
15016 *
15017 * @author Vova Feldman (@svovaf)
15018 * @since 2.0.0
15019 *
15020 * @return FS_User
15021 */
15022 function get_network_user() {
15023 if ( ! $this->_is_network_active ) {
15024 return null;
15025 }
15026
15027 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15028 self::_get_user_by_id( $this->_storage->network_user_id ) :
15029 null;
15030 }
15031
15032 /**
15033 * Returns the current context user or the network's main user.
15034 *
15035 * @author Vova Feldman (@svovaf)
15036 * @since 2.0.0
15037 *
15038 * @return FS_User
15039 */
15040 function get_current_or_network_user() {
15041 return ( $this->_user instanceof FS_User ) ?
15042 $this->_user :
15043 $this->get_network_user();
15044 }
15045
15046 /**
15047 * Returns the main install associated with the network.
15048 *
15049 * @author Vova Feldman (@svovaf)
15050 * @since 2.0.0
15051 *
15052 * @return FS_Site
15053 */
15054 function get_network_install() {
15055 if ( ! $this->_is_network_active ) {
15056 return null;
15057 }
15058
15059 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15060 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15061 null;
15062 }
15063
15064 /**
15065 * Returns the blog ID that is associated with the main install.
15066 *
15067 * @author Leo Fajardo (@leorw)
15068 * @since 2.0.0
15069 *
15070 * @return int|null
15071 */
15072 function get_network_install_blog_id() {
15073 if ( ! $this->_is_network_active ) {
15074 return null;
15075 }
15076
15077 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15078 $this->_storage->network_install_blog_id :
15079 null;
15080 }
15081
15082 /**
15083 * Returns the current context install or the network's main install.
15084 *
15085 * @author Vova Feldman (@svovaf)
15086 * @since 2.0.0
15087 *
15088 * @return FS_Site
15089 */
15090 function get_current_or_network_install() {
15091 return ( $this->_site instanceof FS_Site ) ?
15092 $this->_site :
15093 $this->get_network_install();
15094 }
15095
15096 /**
15097 * Check if executing a site level action from the network level admin.
15098 *
15099 * @author Vova Feldman (@svovaf)
15100 * @since 2.0.0
15101 *
15102 * @return false|int If yes, return the requested blog ID.
15103 */
15104 private function is_network_level_site_specific_action() {
15105 if ( ! $this->_is_network_active ) {
15106 return false;
15107 }
15108
15109 if ( ! fs_is_network_admin() ) {
15110 return false;
15111 }
15112
15113 $blog_id = fs_request_get( 'blog_id', '' );
15114
15115 return is_numeric( $blog_id ) ? $blog_id : false;
15116 }
15117
15118 /**
15119 * Check if executing an action from the network level admin.
15120 *
15121 * @author Vova Feldman (@svovaf)
15122 * @since 2.0.0
15123 *
15124 * @return bool
15125 */
15126 private function is_network_level_action() {
15127 return ( $this->_is_network_active && fs_is_network_admin() );
15128 }
15129
15130 /**
15131 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
15132 * 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.
15133 *
15134 * @author Vova Feldman (@svovaf)
15135 * @since 2.0.0
15136 *
15137 * @param int $context_blog_id
15138 */
15139 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
15140 $this->_logger->entrance();
15141
15142 if ( $this->_is_network_active ) {
15143 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
15144 $installs_map = $this->get_blog_install_map();
15145
15146 foreach ( $installs_map as $blog_id => $install ) {
15147 /**
15148 * @var FS_Site $install
15149 */
15150 if ( $context_blog_id == $blog_id ) {
15151 continue;
15152 }
15153
15154 if ( $install->user_id != $this->_storage->network_user_id ) {
15155 continue;
15156 }
15157
15158 // Switch reference to a blog that is opted-in and belong to the same super-admin.
15159 $this->_storage->network_install_blog_id = $blog_id;
15160 break;
15161 }
15162 }
15163 }
15164
15165 if ( $this->is_sync_cron_scheduled() &&
15166 $context_blog_id == $this->get_sync_cron_blog_id()
15167 ) {
15168 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
15169 }
15170
15171 if ( $this->is_install_sync_scheduled() &&
15172 $context_blog_id == $this->get_install_sync_cron_blog_id()
15173 ) {
15174 $this->schedule_install_sync( $context_blog_id );
15175 }
15176 }
15177
15178 /**
15179 * Executed after site deactivation, archive, or flag as spam.
15180 *
15181 * @author Vova Feldman (@svovaf)
15182 * @since 2.0.0
15183 *
15184 * @param int $context_blog_id
15185 */
15186 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
15187 $this->_logger->entrance();
15188
15189 $install = $this->get_install_by_blog_id( $context_blog_id );
15190
15191 if ( ! is_object( $install ) ) {
15192 // Site not connected.
15193 return;
15194 }
15195
15196 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15197
15198 $current_blog_id = get_current_blog_id();
15199
15200 $this->switch_to_blog( $context_blog_id );
15201
15202 // Send deactivation event.
15203 $this->sync_install( array(
15204 'is_active' => false,
15205 ) );
15206
15207 $this->switch_to_blog( $current_blog_id );
15208 }
15209
15210 /**
15211 * Executed after site deletion.
15212 *
15213 * @author Vova Feldman (@svovaf)
15214 * @since 2.0.0
15215 *
15216 * @param int $context_blog_id
15217 * @param bool $drop True if site's database tables should be dropped. Default is false.
15218 */
15219 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
15220 $this->_logger->entrance();
15221
15222 $install = $this->get_install_by_blog_id( $context_blog_id );
15223
15224 if ( ! is_object( $install ) ) {
15225 // Site not connected.
15226 return;
15227 }
15228
15229 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
15230
15231 $current_blog_id = get_current_blog_id();
15232
15233 $this->switch_to_blog( $context_blog_id );
15234
15235 if ( $drop ) {
15236 // Delete install if dropping site DB.
15237 $this->delete_account_event();
15238 } else {
15239 // Send deactivation event.
15240 $this->sync_install( array(
15241 'is_active' => false,
15242 ) );
15243 }
15244
15245 $this->switch_to_blog( $current_blog_id );
15246 }
15247
15248 /**
15249 * Executed after site re-activation.
15250 *
15251 * @author Vova Feldman (@svovaf)
15252 * @since 2.0.0
15253 *
15254 * @param int $context_blog_id
15255 */
15256 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
15257 $this->_logger->entrance();
15258
15259 $install = $this->get_install_by_blog_id( $context_blog_id );
15260
15261 if ( ! is_object( $install ) ) {
15262 // Site not connected.
15263 return;
15264 }
15265
15266 if ( ! self::is_site_active( $context_blog_id ) ) {
15267 // Site not yet active (can be in spam mode, archived, deleted...).
15268 return;
15269 }
15270
15271 $current_blog_id = get_current_blog_id();
15272
15273 $this->switch_to_blog( $context_blog_id );
15274
15275 // Send re-activation event.
15276 $this->sync_install( array(
15277 'is_active' => true,
15278 ) );
15279
15280 $this->switch_to_blog( $current_blog_id );
15281 }
15282
15283 #endregion Multisite
15284
15285 /**
15286 * @author Leo Fajardo (@leorw)
15287 *
15288 * @param string $path
15289 * @param string $scheme
15290 * @param bool $network
15291 *
15292 * @return string
15293 */
15294 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
15295 return ( $this->_is_network_active && $network ) ?
15296 network_admin_url( $path, $scheme ) :
15297 admin_url( $path, $scheme );
15298 }
15299
15300 /**
15301 * Check if currently in a specified admin page.
15302 *
15303 * @author Vova Feldman (@svovaf)
15304 * @since 1.2.2.7
15305 *
15306 * @param string $page
15307 *
15308 * @return bool
15309 */
15310 function is_admin_page( $page ) {
15311 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
15312 }
15313
15314 /**
15315 * Check if currently in the product's main admin page.
15316 *
15317 * @author Vova Feldman (@svovaf)
15318 * @since 2.3.1
15319 *
15320 * @return bool
15321 */
15322 function is_main_admin_page() {
15323 return $this->is_admin_page( '' );
15324 }
15325
15326 /**
15327 * Get module's main admin setting page URL.
15328 *
15329 * @author Vova Feldman (@svovaf)
15330 * @since 1.2.2.7
15331 *
15332 * @return string
15333 */
15334 function main_menu_url() {
15335 return $this->_menu->main_menu_url();
15336 }
15337
15338 /**
15339 * Check if currently on the theme's setting page or
15340 * on any of the Freemius added pages (via tabs).
15341 *
15342 * @author Vova Feldman (@svovaf)
15343 * @since 1.2.2.7
15344 *
15345 * @return bool
15346 *
15347 * @deprecated Please use is_product_settings_page() instead;
15348 */
15349 function is_theme_settings_page() {
15350 return $this->is_product_settings_page();
15351 }
15352
15353 /**
15354 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
15355 *
15356 * @author Vova Feldman (@svovaf)
15357 * @since 1.2.2.7
15358 *
15359 * @return bool
15360 */
15361 function is_product_settings_page() {
15362 return fs_starts_with(
15363 fs_request_get( 'page', '', 'get' ),
15364 $this->_menu->get_slug()
15365 );
15366 }
15367
15368 /**
15369 * Plugin's account page + sync license URL.
15370 *
15371 * @author Vova Feldman (@svovaf)
15372 * @since 1.1.9.1
15373 *
15374 * @param bool|number $plugin_id
15375 * @param bool $add_action_nonce
15376 * @param array $params
15377 *
15378 * @return string
15379 */
15380 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
15381 if ( is_numeric( $plugin_id ) ) {
15382 $params['plugin_id'] = $plugin_id;
15383 }
15384
15385 return $this->get_account_url(
15386 $this->get_unique_affix() . '_sync_license',
15387 $params,
15388 $add_action_nonce
15389 );
15390 }
15391
15392 /**
15393 * Plugin's account URL.
15394 *
15395 * @author Vova Feldman (@svovaf)
15396 * @since 1.0.4
15397 *
15398 * @param bool|string $action
15399 * @param array $params
15400 *
15401 * @param bool $add_action_nonce
15402 *
15403 * @return string
15404 */
15405 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
15406 if ( is_string( $action ) ) {
15407 $params['fs_action'] = $action;
15408 }
15409
15410 self::require_pluggable_essentials();
15411
15412 return ( $add_action_nonce && is_string( $action ) ) ?
15413 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
15414 $this->_get_admin_page_url( 'account', $params );
15415 }
15416
15417 /**
15418 * @author Vova Feldman (@svovaf)
15419 * @since 1.2.0
15420 *
15421 * @param string $tab
15422 * @param bool $action
15423 * @param array $params
15424 * @param bool $add_action_nonce
15425 *
15426 * @return string
15427 *
15428 * @uses get_account_url()
15429 */
15430 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
15431 $params['tab'] = $tab;
15432
15433 return $this->get_account_url( $action, $params, $add_action_nonce );
15434 }
15435
15436 /**
15437 * Plugin's account URL.
15438 *
15439 * @author Vova Feldman (@svovaf)
15440 * @since 1.0.4
15441 *
15442 * @param bool|string $topic
15443 * @param bool|string $message
15444 *
15445 * @return string
15446 */
15447 function contact_url( $topic = false, $message = false ) {
15448 $params = array();
15449 if ( is_string( $topic ) ) {
15450 $params['topic'] = $topic;
15451 }
15452 if ( is_string( $message ) ) {
15453 $params['message'] = $message;
15454 }
15455
15456 if ( $this->is_addon() ) {
15457 $params['addon_id'] = $this->get_id();
15458
15459 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
15460 } else {
15461 return $this->_get_admin_page_url( 'contact', $params );
15462 }
15463 }
15464
15465 /**
15466 * Add-on direct info URL.
15467 *
15468 * @author Vova Feldman (@svovaf)
15469 * @since 1.1.0
15470 *
15471 * @param string $slug
15472 *
15473 * @return string
15474 */
15475 function addon_url( $slug ) {
15476 return $this->_get_admin_page_url( 'addons', array(
15477 'slug' => $slug
15478 ) );
15479 }
15480
15481 /**
15482 * Add-ons URL.
15483 *
15484 * @author Vova Feldman (@svovaf)
15485 * @since 2.4.5
15486 *
15487 * @return string
15488 */
15489 function get_addons_url() {
15490 return $this->_get_admin_page_url( 'addons' );
15491 }
15492
15493 /* Logger
15494 ------------------------------------------------------------------------------------------------------------------*/
15495 /**
15496 * @param string $id
15497 * @param bool $prefix_slug
15498 *
15499 * @return FS_Logger
15500 */
15501 function get_logger( $id = '', $prefix_slug = true ) {
15502 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
15503 }
15504
15505 /**
15506 * Note: This method is used externally so don't delete it.
15507 *
15508 * @param $id
15509 * @param bool $load_options
15510 * @param bool $prefix_slug
15511 *
15512 * @return FS_Option_Manager
15513 */
15514 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
15515 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
15516 }
15517
15518 /* Security
15519 ------------------------------------------------------------------------------------------------------------------*/
15520 private static function _encrypt( $str ) {
15521 if ( is_null( $str ) ) {
15522 return null;
15523 }
15524
15525 /**
15526 * The encrypt/decrypt functions are used to protect
15527 * the user from messing up with some of the sensitive
15528 * data stored for the module as a JSON in the database.
15529 *
15530 * I used the same suggested hack by the theme review team.
15531 * For more details, look at the function `Base64UrlDecode()`
15532 * in `./sdk/FreemiusBase.php`.
15533 *
15534 * @todo Remove this hack once the base64 error is removed from the Theme Check.
15535 *
15536 * @author Vova Feldman (@svovaf)
15537 * @since 1.2.2
15538 */
15539 $fn = 'base64' . '_encode';
15540
15541 return $fn( $str );
15542 }
15543
15544 static function _decrypt( $str ) {
15545 if ( is_null( $str ) ) {
15546 return null;
15547 }
15548
15549 /**
15550 * The encrypt/decrypt functions are used to protect
15551 * the user from messing up with some of the sensitive
15552 * data stored for the module as a JSON in the database.
15553 *
15554 * I used the same suggested hack by the theme review team.
15555 * For more details, look at the function `Base64UrlDecode()`
15556 * in `./sdk/FreemiusBase.php`.
15557 *
15558 * @todo Remove this hack once the base64 error is removed from the Theme Check.
15559 *
15560 * @author Vova Feldman (@svovaf)
15561 * @since 1.2.2
15562 */
15563 $fn = 'base64' . '_decode';
15564
15565 return $fn( $str );
15566 }
15567
15568 /**
15569 * @author Vova Feldman (@svovaf)
15570 * @since 1.0.5
15571 *
15572 * @param FS_Entity $entity
15573 *
15574 * @return FS_Entity Return an encrypted clone entity.
15575 */
15576 private static function _encrypt_entity( FS_Entity $entity ) {
15577 $clone = clone $entity;
15578 $props = get_object_vars( $entity );
15579
15580 foreach ( $props as $key => $val ) {
15581 $clone->{$key} = self::_encrypt( $val );
15582 }
15583
15584 return $clone;
15585 }
15586
15587 /**
15588 * @author Vova Feldman (@svovaf)
15589 * @since 1.0.5
15590 *
15591 * @param FS_Entity $entity
15592 *
15593 * @return FS_Entity Return an decrypted clone entity.
15594 */
15595 private static function decrypt_entity( FS_Entity $entity ) {
15596 $clone = clone $entity;
15597 $props = get_object_vars( $entity );
15598
15599 foreach ( $props as $key => $val ) {
15600 $clone->{$key} = self::_decrypt( $val );
15601 }
15602
15603 return $clone;
15604 }
15605
15606 /**
15607 * Tries to activate account based on POST params.
15608 *
15609 * @author Vova Feldman (@svovaf)
15610 * @since 1.0.2
15611 *
15612 * @deprecated Not in use, outdated.
15613 */
15614 function _activate_account() {
15615 if ( $this->is_registered() ) {
15616 // Already activated.
15617 return;
15618 }
15619
15620 self::_clean_admin_content_section();
15621
15622 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
15623 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
15624
15625 // Verify matching plugin details.
15626 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
15627 return;
15628 }
15629
15630 $user = new FS_User();
15631 $user->id = fs_request_get( 'user_id' );
15632 $user->public_key = fs_request_get( 'user_public_key' );
15633 $user->secret_key = fs_request_get( 'user_secret_key' );
15634 $user->email = fs_request_get( 'user_email' );
15635 $user->first = fs_request_get( 'user_first' );
15636 $user->last = fs_request_get( 'user_last' );
15637 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
15638
15639 $site = new FS_Site();
15640 $site->id = fs_request_get( 'install_id' );
15641 $site->public_key = fs_request_get( 'install_public_key' );
15642 $site->secret_key = fs_request_get( 'install_secret_key' );
15643 $site->plan_id = fs_request_get( 'plan_id' );
15644
15645 $plans = array();
15646 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
15647 foreach ( $plans_data as $p ) {
15648 $plan = new FS_Plugin_Plan( $p );
15649 if ( $site->plan_id == $plan->id ) {
15650 $plan->title = fs_request_get( 'plan_title' );
15651 $plan->name = fs_request_get( 'plan_name' );
15652 }
15653
15654 $plans[] = $plan;
15655 }
15656
15657 $this->_set_account( $user, $site, $plans );
15658
15659 // Reload the page with the keys.
15660 fs_redirect( $this->_get_admin_page_url() );
15661 }
15662 }
15663
15664 /**
15665 * @author Vova Feldman (@svovaf)
15666 * @since 1.0.7
15667 *
15668 * @param string $email
15669 *
15670 * @return FS_User|false
15671 */
15672 static function _get_user_by_email( $email ) {
15673 self::$_static_logger->entrance();
15674
15675 $email = trim( strtolower( $email ) );
15676
15677 $users = self::get_all_users();
15678
15679 if ( is_array( $users ) ) {
15680 foreach ( $users as $user ) {
15681 if ( $email === trim( strtolower( $user->email ) ) ) {
15682 return $user;
15683 }
15684 }
15685 }
15686
15687 return false;
15688 }
15689
15690 #----------------------------------------------------------------------------------
15691 #region Account (Loading, Updates & Activation)
15692 #----------------------------------------------------------------------------------
15693
15694 /***
15695 * Load account information (user + site).
15696 *
15697 * @author Vova Feldman (@svovaf)
15698 * @since 1.0.1
15699 */
15700 private function _load_account() {
15701 $this->_logger->entrance();
15702
15703 $this->do_action( 'before_account_load' );
15704
15705 $users = self::get_all_users();
15706 $plans = self::get_all_plans( $this->_module_type );
15707
15708 if ( $this->_logger->is_on() && is_admin() ) {
15709 $this->_logger->log( 'users = ' . var_export( $users, true ) );
15710 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
15711 }
15712
15713 $site = fs_is_network_admin() ?
15714 $this->get_network_install() :
15715 $this->get_install_by_blog_id();
15716
15717 if ( fs_is_network_admin() &&
15718 $this->is_network_active() &&
15719 ! is_object( $site ) &&
15720 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
15721 ) {
15722 $first_install = $this->find_first_install();
15723
15724 if ( is_null( $first_install ) ) {
15725 unset( $this->_storage->network_install_blog_id );
15726 } else {
15727 $site = $first_install['install'];
15728 $this->_storage->network_install_blog_id = $first_install['blog_id'];
15729 }
15730 }
15731
15732 if ( is_object( $site ) &&
15733 is_numeric( $site->id ) &&
15734 is_numeric( $site->user_id ) &&
15735 FS_Plugin_Plan::is_valid_id( $site->plan_id )
15736 ) {
15737 // Load site.
15738 $this->_site = $site;
15739
15740 // Load plans.
15741 $this->_plans = $plans[ $this->_slug ];
15742 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
15743 $this->_sync_plans();
15744 } else {
15745 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
15746 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
15747 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
15748 } else {
15749 unset( $this->_plans[ $i ] );
15750 }
15751 }
15752 }
15753 }
15754
15755 $user = null;
15756 if ( fs_is_network_admin() && $this->_is_network_active ) {
15757 $user = $this->get_network_user();
15758 }
15759
15760 if ( is_object( $user ) ) {
15761 $this->_user = clone $user;
15762 } else if ( $this->_site ) {
15763 $user = self::_get_user_by_id( $this->_site->user_id );
15764
15765 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
15766 /**
15767 * Try to load the previous owner. This recovery is used for the following use-case:
15768 * 1. Opt-in
15769 * 2. Cloning site1 to site2
15770 * 3. Ownership switch in site1 (same applies for site2)
15771 * 4. Install data sync on site2
15772 * 5. Now site2's install is associated with the new owner which does not exists locally.
15773 */
15774 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
15775 }
15776
15777 if ( ! is_object( $user ) ) {
15778 /**
15779 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
15780 */
15781 $user = $this->sync_user_by_current_install();
15782 }
15783
15784 $this->_user = ( $user instanceof FS_User ) ?
15785 clone $user :
15786 null;
15787 }
15788
15789 if ( is_object( $this->_user ) ) {
15790 // Load licenses.
15791 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15792 }
15793
15794 if ( is_object( $this->_site ) ) {
15795 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
15796
15797 if ( $this->_site->version != $this->get_plugin_version() ) {
15798 // If stored install version is different than current installed plugin version,
15799 // then update plugin version event.
15800 $this->update_plugin_version_event();
15801 }
15802 }
15803
15804 if ( true === $this->_storage->require_license_activation &&
15805 ! fs_request_get_bool( 'require_license', true )
15806 ) {
15807 $this->_storage->require_license_activation = false;
15808 }
15809
15810 if ( $this->is_theme() ) {
15811 $this->_register_account_hooks();
15812 }
15813 }
15814
15815 /**
15816 * Special user recovery mechanism.
15817 *
15818 * @author Vova Feldman (@svovaf)
15819 * @since 2.0.0
15820 *
15821 * @param number|null $site_user_id
15822 *
15823 * @return \FS_User|mixed
15824 */
15825 private function sync_user_by_current_install( $site_user_id = null ) {
15826 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
15827 $site_user_id :
15828 $this->_site->user_id;
15829
15830 $api = $this->get_api_site_scope();
15831
15832 $uid = $this->get_anonymous_id();
15833 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
15834
15835 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
15836
15837 if ( $this->is_api_result_entity( $result ) ) {
15838 $user = new FS_User( $result );
15839 $this->_user = $user;
15840 $this->_store_user();
15841
15842 return $user;
15843 }
15844
15845 $error_code = FS_Api::get_error_code( $result );
15846
15847 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
15848 /**
15849 * Those API errors will continue coming and are not recoverable with the
15850 * current site's data. Therefore, extend the API call's cached result to 7 days.
15851 */
15852 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
15853 }
15854
15855 return $result;
15856 }
15857
15858 /**
15859 * @author Vova Feldman (@svovaf)
15860 * @since 1.0.1
15861 *
15862 * @param FS_User $user
15863 * @param FS_Site $site
15864 * @param bool|array $plans
15865 */
15866 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
15867 $site->user_id = $user->id;
15868
15869 $this->_site = $site;
15870 $this->_user = $user;
15871 if ( false !== $plans ) {
15872 $this->_plans = $plans;
15873 }
15874
15875 $this->send_install_update();
15876
15877 $this->_store_account();
15878
15879 }
15880
15881 /**
15882 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
15883 * Each version is trimmed after the 16th char.
15884 *
15885 * @author Vova Feldman (@svovaf)
15886 * @since 2.2.1
15887 *
15888 * @return array
15889 */
15890 private function get_versions() {
15891 $versions = array();
15892 $versions['platform_version'] = get_bloginfo( 'version' );
15893 $versions['sdk_version'] = $this->version;
15894 $versions['programming_language_version'] = phpversion();
15895
15896 foreach ( $versions as $k => $version ) {
15897 if ( is_string( $versions[ $k ] ) && ! empty( $versions[ $k ] ) ) {
15898 $versions[ $k ] = substr( $versions[ $k ], 0, 16 );
15899 }
15900 }
15901
15902 return $versions;
15903 }
15904
15905 /**
15906 * @author Leo Fajardo (@leorw)
15907 * @since 2.3.0
15908 *
15909 * @return bool
15910 */
15911 function has_beta_update() {
15912 return (
15913 ! empty( $this->_storage->beta_data ) &&
15914 ( true === $this->_storage->beta_data['is_beta'] ) &&
15915 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
15916 );
15917 }
15918
15919 /**
15920 * @author Leo Fajardo (@leorw)
15921 * @since 2.3.0
15922 *
15923 * @return bool
15924 */
15925 function is_beta() {
15926 return (
15927 ! empty( $this->_storage->beta_data ) &&
15928 ( true === $this->_storage->beta_data['is_beta'] ) &&
15929 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
15930 );
15931 }
15932
15933 /**
15934 * @author Leo Fajardo (@leorw)
15935 * @since 2.3.0
15936 */
15937 private function sync_user_beta_mode() {
15938 $user = $this->get_api_user_scope()->get( '/?plugin_id=' . $this->get_id() . '&fields=is_beta' );
15939
15940 if ( $this->is_api_result_entity( $user ) ) {
15941 $this->_user->is_beta = $user->is_beta;
15942 $this->_store_user();
15943 }
15944 }
15945
15946 /**
15947 * @author Vova Feldman (@svovaf)
15948 * @since 1.1.7.4
15949 *
15950 * @param array $override_with
15951 * @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.
15952 *
15953 * @return array
15954 */
15955 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
15956 $this->_logger->entrance();
15957
15958 $current_user = self::_get_current_wp_user();
15959
15960 $activation_action = $this->get_unique_affix() . '_activate_new';
15961 $return_url = $this->is_anonymous() ?
15962 // If skipped already, then return to the account page.
15963 $this->get_account_url( $activation_action, array(), false ) :
15964 // Return to the module's main page.
15965 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
15966
15967 $versions = $this->get_versions();
15968
15969 $params = array_merge( $versions, array(
15970 'user_firstname' => $current_user->user_firstname,
15971 'user_lastname' => $current_user->user_lastname,
15972 'user_nickname' => $current_user->user_nicename,
15973 'user_email' => $current_user->user_email,
15974 'user_ip' => WP_FS__REMOTE_ADDR,
15975 'plugin_slug' => $this->_slug,
15976 'plugin_id' => $this->get_id(),
15977 'plugin_public_key' => $this->get_public_key(),
15978 'plugin_version' => $this->get_plugin_version(),
15979 'return_url' => fs_nonce_url( $return_url, $activation_action ),
15980 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
15981 'account',
15982 array( 'fs_action' => 'sync_user' )
15983 ), 'sync_user' ),
15984 'is_premium' => $this->is_premium(),
15985 'is_active' => true,
15986 'is_uninstalled' => false,
15987 ) );
15988
15989 if ( $this->is_addon() ) {
15990 $parent_fs = $this->get_parent_instance();
15991
15992 $params['parent_plugin_slug'] = $parent_fs->_slug;
15993 $params['parent_plugin_id'] = $parent_fs->get_id();
15994 }
15995
15996 if ( true === $network_level_or_blog_id ) {
15997 if ( ! isset( $override_with['sites'] ) ) {
15998 $params['sites'] = $this->get_sites_for_network_level_optin();
15999 }
16000 } else {
16001 $site = is_numeric( $network_level_or_blog_id ) ?
16002 array( 'blog_id' => $network_level_or_blog_id ) :
16003 null;
16004
16005 $site = $this->get_site_info( $site );
16006
16007 $params = array_merge( $params, array(
16008 'site_uid' => $site['uid'],
16009 'site_url' => $site['url'],
16010 'site_name' => $site['title'],
16011 'language' => $site['language'],
16012 'charset' => $site['charset'],
16013 ) );
16014 }
16015
16016 if ( $this->is_pending_activation() &&
16017 ! empty( $this->_storage->pending_license_key )
16018 ) {
16019 $params['license_key'] = $this->_storage->pending_license_key;
16020 }
16021
16022 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
16023 // Even though rand() is known for its security issues,
16024 // the timestamp adds another layer of protection.
16025 // It would be very hard for an attacker to get the secret key form here.
16026 // Plus, this should never run in production since the secret should never
16027 // be included in the production version.
16028 $params['ts'] = WP_FS__SCRIPT_START_TIME;
16029 $params['salt'] = md5( uniqid( rand() ) );
16030 $params['secure'] = md5(
16031 $params['ts'] .
16032 $params['salt'] .
16033 $this->get_secret_key()
16034 );
16035 }
16036
16037 return array_merge( $params, $override_with );
16038 }
16039
16040 /**
16041 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
16042 * 2. If there was an API error, return the API result.
16043 *
16044 * @author Vova Feldman (@svovaf)
16045 * @since 1.1.7.4
16046 *
16047 * @param string|bool $email
16048 * @param string|bool $first
16049 * @param string|bool $last
16050 * @param string|bool $license_key
16051 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
16052 * In this case, the user and site info will be sent to the server but no
16053 * data will be saved to the WP installation's database.
16054 * @param number|bool $trial_plan_id
16055 * @param bool $is_disconnected Whether or not to opt in without tracking.
16056 * @param null|bool $is_marketing_allowed
16057 * @param array $sites If network-level opt-in, an array of containing details of sites.
16058 *
16059 * @return string|object
16060 * @use WP_Error
16061 */
16062 function opt_in(
16063 $email = false,
16064 $first = false,
16065 $last = false,
16066 $license_key = false,
16067 $is_uninstall = false,
16068 $trial_plan_id = false,
16069 $is_disconnected = false,
16070 $is_marketing_allowed = null,
16071 $sites = array()
16072 ) {
16073 $this->_logger->entrance();
16074
16075 if ( false === $email ) {
16076 $current_user = self::_get_current_wp_user();
16077 $email = $current_user->user_email;
16078 }
16079
16080 /**
16081 * @since 1.2.1 If activating with license key, ignore the context-user
16082 * since the user will be automatically loaded from the license.
16083 */
16084 if ( empty( $license_key ) ) {
16085 // Clean up pending license if opt-ing in again.
16086 $this->_storage->remove( 'pending_license_key' );
16087
16088 if ( ! $is_uninstall ) {
16089 $fs_user = Freemius::_get_user_by_email( $email );
16090 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
16091 return $this->install_with_user(
16092 $fs_user,
16093 false,
16094 $trial_plan_id,
16095 true,
16096 true,
16097 $sites
16098 );
16099 }
16100 }
16101 }
16102
16103 $user_info = array();
16104 if ( ! empty( $email ) ) {
16105 $user_info['user_email'] = $email;
16106 }
16107 if ( ! empty( $first ) ) {
16108 $user_info['user_firstname'] = $first;
16109 }
16110 if ( ! empty( $last ) ) {
16111 $user_info['user_lastname'] = $last;
16112 }
16113
16114 if ( ! empty( $sites ) ) {
16115 $is_network = true;
16116
16117 $user_info['sites'] = $sites;
16118 } else {
16119 $is_network = false;
16120 }
16121
16122 $params = $this->get_opt_in_params( $user_info, $is_network );
16123
16124 $filtered_license_key = false;
16125 if ( is_string( $license_key ) ) {
16126 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
16127 $params['license_key'] = $filtered_license_key;
16128 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
16129 $params['trial_plan_id'] = $trial_plan_id;
16130 }
16131
16132 if ( $is_uninstall ) {
16133 $params['uninstall_params'] = array(
16134 'reason_id' => $this->_storage->uninstall_reason->id,
16135 'reason_info' => $this->_storage->uninstall_reason->info
16136 );
16137 }
16138
16139 if ( isset( $params['license_key'] ) ) {
16140 $fs_user = Freemius::_get_user_by_email( $email );
16141
16142 if ( is_object( $fs_user ) ) {
16143 /**
16144 * If opting in with a context license and the context WP Admin user already opted in
16145 * before from the current site, add the user context security params to avoid the
16146 * unnecessary email activation when the context license is owned by the same context user.
16147 *
16148 * @author Leo Fajardo (@leorw)
16149 * @since 1.2.3
16150 */
16151 $params = array_merge( $params, FS_Security::instance()->get_context_params(
16152 $fs_user,
16153 false,
16154 'install_with_existing_user'
16155 ) );
16156 }
16157 }
16158
16159 if ( is_bool( $is_marketing_allowed ) ) {
16160 $params['is_marketing_allowed'] = $is_marketing_allowed;
16161 }
16162
16163 $params['is_disconnected'] = $is_disconnected;
16164 $params['format'] = 'json';
16165
16166 $request = array(
16167 'method' => 'POST',
16168 'body' => $params,
16169 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
16170 );
16171
16172 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
16173 $response = self::safe_remote_post( $url, $request );
16174
16175 if ( is_wp_error( $response ) ) {
16176 /**
16177 * @var WP_Error $response
16178 */
16179 $result = new stdClass();
16180
16181 $error_code = $response->get_error_code();
16182 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
16183
16184 $result->error = (object) array(
16185 'type' => $error_type,
16186 'message' => $response->get_error_message(),
16187 'code' => $error_code,
16188 'http' => 402
16189 );
16190
16191 return $result;
16192 }
16193
16194 // Module is being uninstalled, don't handle the returned data.
16195 if ( $is_uninstall ) {
16196 return true;
16197 }
16198
16199 /**
16200 * 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.
16201 *
16202 * @author Vova Feldman (@svovaf)
16203 * @since 1.2.3
16204 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
16205 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
16206 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
16207 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
16208 */
16209 $decoded = is_string( $response['body'] ) ?
16210 json_decode( $response['body'] ) :
16211 null;
16212
16213 if ( empty( $decoded ) ) {
16214 return false;
16215 }
16216
16217 if ( ! $this->is_api_result_object( $decoded ) ) {
16218 if ( ! empty( $params['license_key'] ) ) {
16219 // Pass the fully entered license key to the failure handler.
16220 $params['license_key'] = $license_key;
16221 }
16222
16223 return $is_uninstall ?
16224 $decoded :
16225 $this->apply_filters( 'after_install_failure', $decoded, $params );
16226 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
16227 if ( $is_network ) {
16228 $site_ids = array();
16229 foreach ( $sites as $site ) {
16230 $site_ids[] = $site['blog_id'];
16231 }
16232
16233 /**
16234 * Store the sites so that they can be installed once the user has clicked on the activation link
16235 * in the email.
16236 *
16237 * @author Leo Fajardo (@leorw)
16238 */
16239 $this->_storage->pending_sites_info = array(
16240 'blog_ids' => $site_ids,
16241 'license_key' => $license_key,
16242 'trial_plan_id' => $trial_plan_id
16243 );
16244 }
16245
16246 // Pending activation, add message.
16247 return $this->set_pending_confirmation(
16248 ( isset( $decoded->email ) ?
16249 $decoded->email :
16250 true ),
16251 false,
16252 $filtered_license_key,
16253 ! empty( $params['trial_plan_id'] )
16254 );
16255 } else if ( isset( $decoded->install_secret_key ) ) {
16256 return $this->install_with_new_user(
16257 $decoded->user_id,
16258 $decoded->user_public_key,
16259 $decoded->user_secret_key,
16260 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
16261 $decoded->is_marketing_allowed :
16262 null ),
16263 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
16264 $decoded->is_extensions_tracking_allowed :
16265 null ),
16266 $decoded->install_id,
16267 $decoded->install_public_key,
16268 $decoded->install_secret_key,
16269 false
16270 );
16271 } else if ( is_array( $decoded->installs ) ) {
16272 return $this->install_many_with_new_user(
16273 $decoded->user_id,
16274 $decoded->user_public_key,
16275 $decoded->user_secret_key,
16276 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
16277 $decoded->is_marketing_allowed :
16278 null ),
16279 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
16280 $decoded->is_extensions_tracking_allowed :
16281 null ),
16282 $decoded->installs,
16283 false
16284 );
16285 }
16286
16287 return $decoded;
16288 }
16289
16290 /**
16291 * Set user and site identities.
16292 *
16293 * @author Vova Feldman (@svovaf)
16294 * @since 1.0.9
16295 *
16296 * @param FS_User $user
16297 * @param FS_Site $site
16298 * @param bool $redirect
16299 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
16300 * redirect (or return a URL) to the account page with a special parameter to
16301 * trigger the auto installation processes.
16302 *
16303 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16304 */
16305 function setup_account(
16306 FS_User $user,
16307 FS_Site $site,
16308 $redirect = true,
16309 $auto_install = false
16310 ) {
16311 return $this->setup_network_account(
16312 $user,
16313 array( $site ),
16314 $redirect,
16315 $auto_install,
16316 false
16317 );
16318 }
16319
16320 /**
16321 * Set user and site identities.
16322 *
16323 * @author Vova Feldman (@svovaf)
16324 * @since 2.0.0
16325 *
16326 * @param FS_User $user
16327 * @param FS_Site[] $installs
16328 * @param bool $redirect
16329 * @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.
16330 * @param bool $is_network_level_opt_in
16331 *
16332 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16333 */
16334 function setup_network_account(
16335 FS_User $user,
16336 array $installs,
16337 $redirect = true,
16338 $auto_install = false,
16339 $is_network_level_opt_in = true
16340 ) {
16341 $first_install = $installs[0];
16342
16343 $this->_user = $user;
16344 $this->_site = $first_install;
16345
16346 $this->_sync_plans();
16347
16348 if ( $this->_storage->handle_gdpr_admin_notice &&
16349 $this->should_handle_gdpr_admin_notice() &&
16350 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
16351 ) {
16352 /**
16353 * Clear user lock after an opt-in.
16354 */
16355 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
16356 FS_User_Lock::instance()->unlock();
16357 }
16358
16359 if ( 1 < count( $installs ) ) {
16360 // Only network level opt-in can have more than one install.
16361 $is_network_level_opt_in = true;
16362 }
16363 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
16364 // If Freemius was OFF before, turn it on.
16365 $this->turn_on();
16366
16367 $this->handle_account_connection(
16368 $installs,
16369 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
16370 );
16371
16372 if ( is_numeric( $first_install->license_id ) ) {
16373 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
16374 }
16375
16376 $this->_admin_notices->remove_sticky( 'connect_account' );
16377
16378 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
16379 // Remove pending activation sticky notice (if still exist).
16380 $this->_admin_notices->remove_sticky( 'activation_pending' );
16381
16382 // Remove plugin from pending activation mode.
16383 unset( $this->_storage->is_pending_activation );
16384
16385 if ( ! $this->is_paying_or_trial() ) {
16386 $this->_admin_notices->add_sticky(
16387 sprintf( $this->get_text_inline( '%s activation was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
16388 'activation_complete'
16389 );
16390 }
16391 }
16392
16393 if ( $this->is_paying_or_trial() ) {
16394 if ( ! $this->is_premium() ||
16395 ! $this->has_premium_version() ||
16396 ! $this->has_settings_menu()
16397 ) {
16398 if ( $this->is_paying() ) {
16399 $this->_admin_notices->add_sticky(
16400 sprintf(
16401 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
16402 $this->get_plan_title()
16403 ) . $this->get_complete_upgrade_instructions(),
16404 'plan_upgraded',
16405 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16406 );
16407 } else {
16408 $trial_plan = $this->get_trial_plan();
16409
16410 $this->_admin_notices->add_sticky(
16411 sprintf(
16412 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
16413 '<i>' . $this->get_plugin_name() . '</i>'
16414 ) . $this->get_complete_upgrade_instructions( $trial_plan->title ),
16415 'trial_started',
16416 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16417 );
16418 }
16419 }
16420
16421 $this->_admin_notices->remove_sticky( array(
16422 'trial_promotion',
16423 ) );
16424 }
16425
16426 $plugin_id = fs_request_get( 'plugin_id', false );
16427
16428 // Store activation time ONLY for plugins & themes (not add-ons).
16429 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
16430 if ( empty( $this->_storage->activation_timestamp ) ) {
16431 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
16432 }
16433 }
16434
16435 $next_page = '';
16436
16437 $extra = array();
16438 if ( $auto_install ) {
16439 $extra['auto_install'] = 'true';
16440 }
16441
16442 if ( is_numeric( $plugin_id ) ) {
16443 /**
16444 * @author Leo Fajardo (@leorw)
16445 * @since 1.2.1.6
16446 *
16447 * Also sync the license after an anonymous user subscribes.
16448 */
16449 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
16450 // Add-on was installed - sync license right after install.
16451 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
16452 }
16453 } else {
16454 /**
16455 * @author Vova Feldman (@svovaf)
16456 * @since 1.1.9 If site installed with a valid license, sync license.
16457 */
16458 if ( $this->is_paying() ) {
16459 $this->_sync_plugin_license(
16460 true,
16461 // Installs data is already synced in the beginning of this method directly or via _set_account().
16462 false
16463 );
16464 }
16465
16466 // Reload the page with the keys.
16467 $next_page = $this->is_anonymous() ?
16468 // If user previously skipped, redirect to account page.
16469 $this->get_account_url( false, $extra ) :
16470 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
16471 }
16472
16473 if ( ! empty( $next_page ) && $redirect ) {
16474 fs_redirect( $next_page );
16475 }
16476
16477 return $next_page;
16478 }
16479
16480 /**
16481 * Install plugin with new user information after approval.
16482 *
16483 * @author Vova Feldman (@svovaf)
16484 * @since 1.0.7
16485 */
16486 function _install_with_new_user() {
16487 $this->_logger->entrance();
16488
16489 if ( $this->is_registered() ) {
16490 return;
16491 }
16492
16493 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
16494 // @todo This logic should be improved because it's executed on every load of a theme.
16495 $this->is_theme()
16496 ) {
16497 // check_admin_referer( $this->_slug . '_activate_new' );
16498
16499 if ( fs_request_has( 'user_secret_key' ) ) {
16500 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
16501 $pending_sites_info = $this->_storage->pending_sites_info;
16502
16503 $this->install_many_pending_with_user(
16504 fs_request_get( 'user_id' ),
16505 fs_request_get( 'user_public_key' ),
16506 fs_request_get( 'user_secret_key' ),
16507 fs_request_get_bool( 'is_marketing_allowed', null ),
16508 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
16509 $pending_sites_info['blog_ids'],
16510 $pending_sites_info['license_key'],
16511 $pending_sites_info['trial_plan_id']
16512 );
16513 } else {
16514 $this->install_with_new_user(
16515 fs_request_get( 'user_id' ),
16516 fs_request_get( 'user_public_key' ),
16517 fs_request_get( 'user_secret_key' ),
16518 fs_request_get_bool( 'is_marketing_allowed', null ),
16519 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
16520 fs_request_get( 'install_id' ),
16521 fs_request_get( 'install_public_key' ),
16522 fs_request_get( 'install_secret_key' ),
16523 true,
16524 fs_request_get_bool( 'auto_install' )
16525 );
16526 }
16527 } else if ( fs_request_has( 'pending_activation' ) ) {
16528 $this->set_pending_confirmation( fs_request_get( 'user_email' ), true );
16529 }
16530 }
16531 }
16532
16533 /**
16534 * @author Vova Feldman (@svovaf)
16535 * @since 2.0.0
16536 *
16537 * @param number $id
16538 * @param string $public_key
16539 * @param string $secret_key
16540 *
16541 * @return \FS_User
16542 */
16543 private function setup_user( $id, $public_key, $secret_key ) {
16544 $user = self::_get_user_by_id( $id );
16545
16546 if ( is_object( $user ) ) {
16547 $this->_user = $user;
16548 } else {
16549 $user = new FS_User();
16550 $user->id = $id;
16551 $user->public_key = $public_key;
16552 $user->secret_key = $secret_key;
16553
16554 $this->_user = $user;
16555 $user_result = $this->get_api_user_scope()->get();
16556 $user = new FS_User( $user_result );
16557
16558 $this->_user = $user;
16559 $this->_store_user();
16560 }
16561
16562 return $user;
16563 }
16564
16565 /**
16566 * Install plugin with new user.
16567 *
16568 * @author Vova Feldman (@svovaf)
16569 * @since 1.1.7.4
16570 *
16571 * @param number $user_id
16572 * @param string $user_public_key
16573 * @param string $user_secret_key
16574 * @param bool|null $is_marketing_allowed
16575 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
16576 * @param number $install_id
16577 * @param string $install_public_key
16578 * @param string $install_secret_key
16579 * @param bool $redirect
16580 * @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.
16581 *
16582 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16583 */
16584 private function install_with_new_user(
16585 $user_id,
16586 $user_public_key,
16587 $user_secret_key,
16588 $is_marketing_allowed,
16589 $is_extensions_tracking_allowed,
16590 $install_id,
16591 $install_public_key,
16592 $install_secret_key,
16593 $redirect = true,
16594 $auto_install = false
16595 ) {
16596 /**
16597 * This method is also executed after opting in with a license key since the
16598 * license can be potentially associated with a different owner.
16599 *
16600 * @since 2.0.0
16601 */
16602 $user = self::_get_user_by_id( $user_id );
16603
16604 if ( ! is_object( $user ) ) {
16605 $user = new FS_User();
16606 $user->id = $user_id;
16607 $user->public_key = $user_public_key;
16608 $user->secret_key = $user_secret_key;
16609
16610 $this->_user = $user;
16611 $user_result = $this->get_api_user_scope()->get();
16612 $user = new FS_User( $user_result );
16613 }
16614
16615 $this->_user = $user;
16616
16617 $site = new FS_Site();
16618 $site->id = $install_id;
16619 $site->public_key = $install_public_key;
16620 $site->secret_key = $install_secret_key;
16621
16622 $this->_site = $site;
16623 $site_result = $this->get_api_site_scope()->get();
16624 $site = new FS_Site( $site_result );
16625 $this->_site = $site;
16626
16627 if ( ! is_null( $is_marketing_allowed ) ) {
16628 $this->disable_opt_in_notice_and_lock_user();
16629 }
16630
16631 if ( ! is_null( $is_extensions_tracking_allowed ) ) {
16632 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
16633 }
16634
16635 return $this->setup_account(
16636 $this->_user,
16637 $this->_site,
16638 $redirect,
16639 $auto_install
16640 );
16641 }
16642
16643 /**
16644 * Install plugin with user.
16645 *
16646 * @author Leo Fajardo (@leorw)
16647 * @since 2.0.0
16648 *
16649 * @param number $user_id
16650 * @param string $user_public_key
16651 * @param string $user_secret_key
16652 * @param bool|null $is_marketing_allowed
16653 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
16654 * @param array $site_ids
16655 * @param bool $license_key
16656 * @param bool $trial_plan_id
16657 * @param bool $redirect
16658 *
16659 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16660 */
16661 private function install_many_pending_with_user(
16662 $user_id,
16663 $user_public_key,
16664 $user_secret_key,
16665 $is_marketing_allowed,
16666 $is_extensions_tracking_allowed,
16667 $site_ids,
16668 $license_key = false,
16669 $trial_plan_id = false,
16670 $redirect = true
16671 ) {
16672 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
16673
16674 if ( ! is_null( $is_marketing_allowed ) ) {
16675 $this->disable_opt_in_notice_and_lock_user();
16676 }
16677
16678 if ( ! is_null( $is_extensions_tracking_allowed ) ) {
16679 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
16680 }
16681
16682 $sites = array();
16683 foreach ( $site_ids as $site_id ) {
16684 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
16685 }
16686
16687 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
16688 }
16689
16690 /**
16691 * Multi-site install with a new user.
16692 *
16693 * @author Vova Feldman (@svovaf)
16694 * @since 2.0.0
16695 *
16696 * @param number $user_id
16697 * @param string $user_public_key
16698 * @param string $user_secret_key
16699 * @param bool|null $is_marketing_allowed
16700 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
16701 * @param object[] $installs
16702 * @param bool $redirect
16703 * @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.
16704 *
16705 * @return string If redirect is `false`, returns the next page the user should be redirected to.
16706 */
16707 private function install_many_with_new_user(
16708 $user_id,
16709 $user_public_key,
16710 $user_secret_key,
16711 $is_marketing_allowed,
16712 $is_extensions_tracking_allowed,
16713 array $installs,
16714 $redirect = true,
16715 $auto_install = false
16716 ) {
16717 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
16718
16719 if ( ! is_null( $is_marketing_allowed ) ) {
16720 $this->disable_opt_in_notice_and_lock_user();
16721 }
16722
16723 if ( ! is_null( $is_extensions_tracking_allowed ) ) {
16724 $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed );
16725 }
16726
16727 $install_ids = array();
16728
16729 foreach ( $installs as $install ) {
16730 $install_ids[] = $install->id;
16731 }
16732
16733 $left = count( $install_ids );
16734 $offset = 0;
16735
16736 $installs = array();
16737 while ( $left > 0 ) {
16738 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, 25 ) ) );
16739
16740 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
16741 // @todo Handle API error.
16742 }
16743
16744 $installs = array_merge( $installs, $result->installs );
16745
16746 $left -= 25;
16747 }
16748
16749 foreach ( $installs as &$install ) {
16750 $install = new FS_Site( $install );
16751 }
16752
16753 return $this->setup_network_account(
16754 $this->_user,
16755 $installs,
16756 $redirect,
16757 $auto_install
16758 );
16759 }
16760
16761 /**
16762 * @author Vova Feldman (@svovaf)
16763 * @since 1.1.7.4
16764 *
16765 * @param string|bool $email
16766 * @param bool $redirect
16767 * @param string|bool $license_key Since 1.2.1.5
16768 * @param bool $is_pending_trial Since 1.2.1.5
16769 *
16770 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
16771 */
16772 private function set_pending_confirmation(
16773 $email = false,
16774 $redirect = true,
16775 $license_key = false,
16776 $is_pending_trial = false
16777 ) {
16778 if ( $this->_ignore_pending_mode ) {
16779 /**
16780 * If explicitly asked to ignore pending mode, set to anonymous mode
16781 * if require confirmation before finalizing the opt-in.
16782 *
16783 * @author Vova Feldman
16784 * @since 1.2.1.6
16785 */
16786 $this->skip_connection( null, fs_is_network_admin() );
16787 } else {
16788 // Install must be activated via email since
16789 // user with the same email already exist.
16790 $this->_storage->is_pending_activation = true;
16791 $this->_add_pending_activation_notice( $email, $is_pending_trial );
16792 }
16793
16794 if ( ! empty( $license_key ) ) {
16795 $this->_storage->pending_license_key = $license_key;
16796 }
16797
16798 // Remove the opt-in sticky notice.
16799 $this->_admin_notices->remove_sticky( array(
16800 'connect_account',
16801 'trial_promotion',
16802 ) );
16803
16804 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
16805
16806 // Reload the page with with pending activation message.
16807 if ( $redirect ) {
16808 fs_redirect( $next_page );
16809 }
16810
16811 return $next_page;
16812 }
16813
16814 /**
16815 * Install plugin with current logged WP user info.
16816 *
16817 * @author Vova Feldman (@svovaf)
16818 * @since 1.0.7
16819 */
16820 function _install_with_current_user() {
16821 $this->_logger->entrance();
16822
16823 if ( $this->is_registered() ) {
16824 return;
16825 }
16826
16827 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
16828 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
16829
16830 /**
16831 * @author Vova Feldman (@svovaf)
16832 * @since 1.1.9 Add license key if given.
16833 */
16834 $license_key = fs_request_get( 'license_secret_key' );
16835
16836 $this->update_extensions_tracking_flag( fs_request_get_bool( 'is_extensions_tracking_allowed', true ) );
16837
16838 $this->install_with_current_user( $license_key );
16839 }
16840 }
16841
16842
16843 /**
16844 * @author Vova Feldman (@svovaf)
16845 * @since 1.1.7.4
16846 *
16847 * @param string|bool $license_key
16848 * @param number|bool $trial_plan_id
16849 * @param array $sites Since 2.0.0
16850 * @param bool $redirect
16851 *
16852 * @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.
16853 */
16854 private function install_with_current_user(
16855 $license_key = false,
16856 $trial_plan_id = false,
16857 $sites = array(),
16858 $redirect = true
16859 ) {
16860 // Get current logged WP user.
16861 $current_user = self::_get_current_wp_user();
16862
16863 // Find the relevant FS user by the email.
16864 $user = self::_get_user_by_email( $current_user->user_email );
16865
16866 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
16867 }
16868
16869 /**
16870 * @author Vova Feldman (@svovaf)
16871 * @since 2.0.0
16872 *
16873 * @param \FS_User $user
16874 * @param string|bool $license_key
16875 * @param number|bool $trial_plan_id
16876 * @param bool $redirect
16877 * @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.
16878 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
16879 *
16880 * @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.
16881 */
16882 function install_with_user(
16883 FS_User $user,
16884 $license_key = false,
16885 $trial_plan_id = false,
16886 $redirect = true,
16887 $setup_account = true,
16888 $sites = array()
16889 ) {
16890 // We have to set the user before getting user scope API handler.
16891 $this->_user = $user;
16892
16893 // Install the plugin.
16894 $result = $this->create_installs_with_user(
16895 $user,
16896 $license_key,
16897 $trial_plan_id,
16898 $sites,
16899 $redirect
16900 );
16901
16902 if ( ! $this->is_api_result_entity( $result ) &&
16903 ! $this->is_api_result_object( $result, 'installs' )
16904 ) {
16905 // @todo Handler potential API error of the $result
16906 }
16907
16908 if ( empty( $sites ) ) {
16909 $site = new FS_Site( $result );
16910 $this->_site = $site;
16911
16912 if ( ! $setup_account ) {
16913 $this->_store_site();
16914
16915 $this->sync_plan_if_not_exist( $site->plan_id );
16916
16917 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
16918 $this->sync_license_if_not_exist( $site->license_id, $license_key );
16919 }
16920
16921 $this->_admin_notices->remove_sticky( 'connect_account', false );
16922
16923 return $site;
16924 }
16925
16926 return $this->setup_account( $this->_user, $this->_site, $redirect );
16927 } else {
16928 $installs = array();
16929 foreach ( $result->installs as $install ) {
16930 $installs[] = new FS_Site( $install );
16931 }
16932
16933 return $this->setup_network_account(
16934 $user,
16935 $installs,
16936 $redirect
16937 );
16938 }
16939 }
16940
16941 /**
16942 * Initiate an API request to create a collection of installs.
16943 *
16944 * @author Vova Feldman (@svovaf)
16945 * @since 2.0.0
16946 *
16947 * @param \FS_User $user
16948 * @param bool $license_key
16949 * @param bool $trial_plan_id
16950 * @param array $sites
16951 * @param bool $redirect
16952 * @param bool $silent
16953 *
16954 * @return object|mixed
16955 */
16956 private function create_installs_with_user(
16957 FS_User $user,
16958 $license_key = false,
16959 $trial_plan_id = false,
16960 $sites = array(),
16961 $redirect = false,
16962 $silent = false
16963 ) {
16964 $extra_install_params = array(
16965 'uid' => $this->get_anonymous_id(),
16966 'is_disconnected' => false,
16967 );
16968
16969 if ( ! empty( $license_key ) ) {
16970 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
16971
16972 if ( $silent ) {
16973 $extra_install_params['ignore_license_owner'] = true;
16974 }
16975 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
16976 $extra_install_params['trial_plan_id'] = $trial_plan_id;
16977 }
16978
16979 if ( ! empty( $sites ) ) {
16980 $extra_install_params['sites'] = $sites;
16981 }
16982
16983 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
16984
16985 // Install the plugin.
16986 $result = $this->get_api_user_scope_by_user( $user )->call(
16987 "/plugins/{$this->get_id()}/installs.json",
16988 'post',
16989 $args
16990 );
16991
16992 if ( ! $this->is_api_result_entity( $result ) &&
16993 ! $this->is_api_result_object( $result, 'installs' )
16994 ) {
16995 if ( ! empty( $args['license_key'] ) ) {
16996 // Pass the fully entered license key to the failure handler.
16997 $args['license_key'] = $license_key;
16998 }
16999
17000 $result = $this->apply_filters( 'after_install_failure', $result, $args );
17001
17002 if ( ! $silent ) {
17003 $this->_admin_notices->add(
17004 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17005 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
17006 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17007 'error'
17008 );
17009 }
17010
17011 if ( $redirect ) {
17012 /**
17013 * We set the user before getting the user scope API handler, so the user became temporarily
17014 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
17015 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
17016 * activation page based on the return value of `is_registered()`. In addition, in case the
17017 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
17018 * misleading plugin activation errors will be shown on the `Plugins` page.
17019 *
17020 * @author Leo Fajardo (@leorw)
17021 */
17022 $this->_user = null;
17023
17024 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
17025 }
17026 }
17027
17028 return $result;
17029 }
17030
17031 /**
17032 * Tries to activate add-on account based on parent plugin info.
17033 *
17034 * @author Vova Feldman (@svovaf)
17035 * @since 1.0.6
17036 *
17037 * @param Freemius $parent_fs
17038 * @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.
17039 */
17040 private function _activate_addon_account( Freemius $parent_fs, $network_level_or_blog_id = null ) {
17041 if ( $this->is_registered() ) {
17042 // Already activated.
17043 return;
17044 }
17045
17046 /**
17047 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
17048 * already returns the data for the current blog.
17049 *
17050 * @author Leo Fajardo (@leorw)
17051 * @since 2.3.0
17052 */
17053 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
17054 array() :
17055 array( 'uid' => $this->get_anonymous_id() );
17056
17057 $params = $this->get_install_data_for_api(
17058 $uid_param_to_override,
17059 false,
17060 false,
17061 /**
17062 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
17063 * already includes the data for it.
17064 *
17065 * @author Leo Fajardo (@leorw)
17066 * @since 2.3.0
17067 */
17068 ( true !== $network_level_or_blog_id )
17069 );
17070
17071 if ( true === $network_level_or_blog_id ) {
17072 $params['sites'] = $this->get_sites_for_network_level_optin();
17073
17074 if ( empty( $params['sites'] ) ) {
17075 return;
17076 }
17077 }
17078
17079 // Activate add-on with parent plugin credentials.
17080 $result = $parent_fs->get_api_site_scope()->call(
17081 "/addons/{$this->_plugin->id}/installs.json",
17082 'post',
17083 $params
17084 );
17085
17086 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17087 $error_message = FS_Api::is_api_error_object( $result ) ?
17088 $result->error->message :
17089 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
17090
17091 $this->_admin_notices->add(
17092 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17093 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
17094 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17095 'error'
17096 );
17097
17098 return;
17099 }
17100
17101 $addon_installs = $result->installs;
17102 foreach ( $addon_installs as $key => $addon_install ) {
17103 $addon_installs[ $key ] = new FS_Site( $addon_install );
17104 }
17105
17106 $first_install = $addon_installs[0];
17107
17108 // Get user information based on parent's plugin.
17109 $user = $parent_fs->get_user();
17110
17111 // First of all, set site and user info - otherwise we won't
17112 // be able to invoke API calls.
17113 $this->_site = $first_install;
17114 $this->_user = $user;
17115
17116 // Sync add-on plans.
17117 $this->_sync_plans();
17118
17119 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
17120
17121 // Get site's current plan.
17122 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
17123
17124 // Sync licenses.
17125 $this->_sync_licenses();
17126
17127 if ( ! fs_is_network_admin() ) {
17128 // Try to activate premium license.
17129 $this->_activate_license( true );
17130 } else {
17131 $license_id = fs_request_get( 'license_id' );
17132
17133 if ( is_object( $this->_site ) &&
17134 FS_Plugin_License::is_valid_id( $license_id ) &&
17135 $license_id == $this->_site->license_id
17136 ) {
17137 // License is already activated.
17138 return;
17139 }
17140
17141 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
17142 $this->_get_license_by_id( $license_id ) :
17143 $this->_get_available_premium_license();
17144
17145 if ( is_object( $premium_license ) ) {
17146 $this->maybe_network_activate_addon_license( $premium_license );
17147 }
17148 }
17149 }
17150
17151 /**
17152 * @author Leo Fajardo (@leorw)
17153 * @since 2.3.0
17154 *
17155 * @param FS_Site[] $installs
17156 * @param bool $is_site_level
17157 */
17158 private function handle_account_connection( $installs, $is_site_level ) {
17159 $first_install = $installs[0];
17160
17161 if ( $is_site_level ) {
17162 $this->_set_account( $this->_user, $first_install );
17163
17164 $this->do_action( 'after_account_connection', $this->_user, $first_install );
17165 } else {
17166 $this->_store_user();
17167
17168 // Map site addresses to their blog IDs.
17169 $address_to_blog_map = $this->get_address_to_blog_map();
17170
17171 $first_blog_id = null;
17172 $blog_2_install_map = array();
17173 foreach ( $installs as $install ) {
17174 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
17175 $blog_id = $address_to_blog_map[ $address ];
17176
17177 $this->_store_site( true, $blog_id, $install );
17178
17179 if ( is_null( $first_blog_id ) ) {
17180 $first_blog_id = $blog_id;
17181 }
17182
17183 $blog_2_install_map[ $blog_id ] = $install;
17184 }
17185
17186 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
17187 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
17188 ) {
17189 // Store network user.
17190 $this->_storage->network_user_id = $this->_user->id;
17191 }
17192
17193 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
17194 $this->_storage->network_install_blog_id = $first_blog_id;
17195 }
17196
17197 if ( count( $installs ) === count( $address_to_blog_map ) ) {
17198 // Super admin opted in for all sites in the network.
17199 $this->_storage->is_network_connected = true;
17200 }
17201
17202 $this->_store_licenses( false );
17203
17204 self::$_accounts->store();
17205
17206 // Don't sync the installs data on network upgrade
17207 if ( ! $this->network_upgrade_mode_completed() ) {
17208 $this->send_installs_update();
17209 }
17210
17211 // Switch install context back to the first install.
17212 $this->_site = $first_install;
17213
17214 $current_blog = get_current_blog_id();
17215
17216 foreach ( $blog_2_install_map as $blog_id => $install ) {
17217 $this->switch_to_blog( $blog_id );
17218
17219 $this->do_action( 'after_account_connection', $this->_user, $install );
17220 }
17221
17222 $this->switch_to_blog( $current_blog );
17223
17224 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
17225 }
17226 }
17227
17228 /**
17229 * Tries to activate parent account based on add-on's info.
17230 *
17231 * @author Vova Feldman (@svovaf)
17232 * @since 1.2.2.7
17233 *
17234 * @param Freemius $parent_fs
17235 */
17236 private function activate_parent_account( Freemius $parent_fs ) {
17237 if ( ! $this->is_addon() ) {
17238 // This is not an add-on.
17239 return;
17240 }
17241
17242 if ( $parent_fs->is_registered() ) {
17243 // Already activated.
17244 return;
17245 }
17246
17247 // Activate parent with add-on's user credentials.
17248 $parent_install = $this->get_api_user_scope()->call(
17249 "/plugins/{$parent_fs->_plugin->id}/installs.json",
17250 'post',
17251 $parent_fs->get_install_data_for_api( array(
17252 'uid' => $parent_fs->get_anonymous_id(),
17253 ), false, false )
17254 );
17255
17256 if ( isset( $parent_install->error ) ) {
17257 $this->_admin_notices->add(
17258 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
17259 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
17260 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17261 'error'
17262 );
17263
17264 return;
17265 }
17266
17267 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
17268
17269 if ( $parent_fs->is_pending_activation() ) {
17270 $parent_fs->_admin_notices->remove_sticky( 'activation_pending' );
17271
17272 unset( $parent_fs->_storage->is_pending_activation );
17273 }
17274
17275 // Get user information based on parent's plugin.
17276 $user = $this->get_user();
17277
17278 // First of all, set site info - otherwise we won't
17279 // be able to invoke API calls.
17280 $parent_fs->_site = new FS_Site( $parent_install );
17281 $parent_fs->_user = $user;
17282
17283 // Sync add-on plans.
17284 $parent_fs->_sync_plans();
17285
17286 $parent_fs->_set_account( $user, $parent_fs->_site );
17287 }
17288
17289 #endregion
17290
17291 #----------------------------------------------------------------------------------
17292 #region Admin Menu Items
17293 #----------------------------------------------------------------------------------
17294
17295 private $_menu_items = array();
17296
17297 /**
17298 * @author Vova Feldman (@svovaf)
17299 * @since 1.2.1.8
17300 *
17301 * @return array
17302 */
17303 function get_menu_items() {
17304 return $this->_menu_items;
17305 }
17306
17307 /**
17308 * @author Vova Feldman (@svovaf)
17309 * @since 1.0.7
17310 *
17311 * @return string
17312 */
17313 function get_menu_slug() {
17314 return $this->_menu->get_slug();
17315 }
17316
17317 /**
17318 * @author Vova Feldman (@svovaf)
17319 * @since 1.0.9
17320 */
17321 function _prepare_admin_menu() {
17322 // if ( ! $this->is_on() ) {
17323 // return;
17324 // }
17325
17326 /**
17327 * When running from a site admin with a network activated module and the connection
17328 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
17329 * site level settings.
17330 *
17331 * @author Vova Feldman (@svovaf)
17332 * @since 2.0.0
17333 */
17334 $should_hide_site_admin_settings = (
17335 $this->_is_network_active &&
17336 ! fs_is_network_admin() &&
17337 ! $this->is_delegated_connection() &&
17338 ! $this->is_anonymous() &&
17339 ! $this->is_registered()
17340 );
17341
17342 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
17343
17344 if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
17345 $should_hide_site_admin_settings
17346 ) {
17347 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
17348 } else {
17349 $this->do_action( fs_is_network_admin() ?
17350 'before_network_admin_menu_init' :
17351 'before_admin_menu_init'
17352 );
17353
17354 $this->add_menu_action();
17355
17356 $this->add_network_menu_when_missing();
17357
17358 $this->add_submenu_items();
17359 }
17360 }
17361
17362 /**
17363 * Admin dashboard menu items modifications.
17364 *
17365 * NOTE: admin_menu action executed before admin_init.
17366 *
17367 * @author Vova Feldman (@svovaf)
17368 * @since 1.0.7
17369 *
17370 */
17371 private function add_menu_action() {
17372 if ( $this->is_activation_mode() ) {
17373 if ( $this->show_opt_in_on_setting_page() ) {
17374 $this->override_plugin_menu_with_activation();
17375 } else {
17376 /**
17377 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
17378 */
17379 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
17380 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
17381 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
17382 fs_request_get_bool( 'pending_activation' )
17383 ) {
17384 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
17385 }
17386 }
17387 } else {
17388 if ( ! $this->is_registered() ) {
17389 // If not registered try to install user.
17390 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
17391 $this->_install_with_new_user();
17392 }
17393 } else if (
17394 fs_request_is_action( 'sync_user' ) &&
17395 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
17396 ) {
17397 $this->_handle_account_user_sync();
17398 }
17399 }
17400 }
17401
17402 /**
17403 * @author Vova Feldman (@svovaf)
17404 * @since 1.0.1
17405 */
17406 function _redirect_on_clicked_menu_link() {
17407 $this->_logger->entrance();
17408
17409 $page = fs_request_get('page');
17410 $page = is_string($page) ? strtolower($page) : '';
17411
17412 $this->_logger->log( 'page = ' . $page );
17413
17414 foreach ( $this->_menu_items as $priority => $items ) {
17415 foreach ( $items as $item ) {
17416 if ( isset( $item['url'] ) ) {
17417 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
17418 $this->_logger->log( 'Redirecting to ' . $item['url'] );
17419
17420 fs_redirect( $item['url'] );
17421 }
17422 }
17423 }
17424 }
17425 }
17426
17427 /**
17428 * Remove plugin's all admin menu items & pages, and replace with activation page.
17429 *
17430 * @author Vova Feldman (@svovaf)
17431 * @since 1.0.1
17432 */
17433 private function override_plugin_menu_with_activation() {
17434 $this->_logger->entrance();
17435
17436 $hook = false;
17437
17438 if ( ! $this->has_settings_menu() ) {
17439 // Add the opt-in page without a menu item.
17440 $hook = FS_Admin_Menu_Manager::add_subpage(
17441 null,
17442 $this->get_plugin_name(),
17443 $this->get_plugin_name(),
17444 'manage_options',
17445 $this->_slug,
17446 array( &$this, '_connect_page_render' )
17447 );
17448 } else if ( $this->_menu->is_top_level() ) {
17449 if ( $this->_menu->is_override_exact() ) {
17450 // Make sure the current page is matching the activation page.
17451 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
17452 return;
17453 }
17454 }
17455
17456 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
17457
17458 if ( false === $hook ) {
17459 // Create new menu item just for the opt-in.
17460 $hook = FS_Admin_Menu_Manager::add_page(
17461 $this->get_plugin_name(),
17462 $this->get_plugin_name(),
17463 'manage_options',
17464 $this->_menu->get_slug(),
17465 array( &$this, '_connect_page_render' )
17466 );
17467 }
17468 } else {
17469 $menus = array( $this->_menu->get_parent_slug() );
17470
17471 if ( $this->_menu->is_override_exact() ) {
17472 // Make sure the current page is matching the activation page.
17473 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
17474 return;
17475 }
17476 }
17477
17478 foreach ( $menus as $parent_slug ) {
17479 $hook = $this->_menu->override_submenu_action(
17480 $parent_slug,
17481 $this->_menu->get_raw_slug(),
17482 array( &$this, '_connect_page_render' )
17483 );
17484
17485 if ( false !== $hook ) {
17486 // Found plugin's submenu item.
17487 break;
17488 }
17489 }
17490 }
17491
17492 if ( $this->is_activation_page() ) {
17493 // Clean admin page from distracting content.
17494 self::_clean_admin_content_section();
17495 }
17496
17497 if ( false !== $hook ) {
17498 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
17499 $this->_install_with_current_user();
17500 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
17501 $this->_install_with_new_user();
17502 }
17503 }
17504 }
17505
17506 /**
17507 * If a plugin was network activated and connected but don't have a network
17508 * level settings, then add an artificial menu item for the Account and other
17509 * Freemius settings.
17510 *
17511 * @author Vova Feldman (@svovaf)
17512 * @since 2.0.0
17513 */
17514 private function add_network_menu_when_missing() {
17515 $this->_logger->entrance();
17516
17517 if ( ! $this->_is_network_active ) {
17518 // Plugin wasn't activated on the network level.
17519 return;
17520 }
17521
17522 if ( ! fs_is_network_admin() ) {
17523 // The context is not the network admin.
17524 return;
17525 }
17526
17527 if ( $this->_menu->has_network_menu() ) {
17528 // Plugin already has a network level menu.
17529 return;
17530 }
17531
17532 if ( $this->is_network_activation_mode() ) {
17533 /**
17534 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
17535 * screen is being shown.
17536 *
17537 * @author Leo Fajardo (@leorw)
17538 */
17539 return;
17540 }
17541
17542 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
17543 if ( $this->is_network_delegated_connection() ) {
17544 // Super-admin delegated the connection to the site admins.
17545 return;
17546 }
17547 }
17548
17549 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
17550
17551 if ( $this->_menu->has_menu() ||
17552 ! $this->is_addon() ||
17553 $this->is_activation_mode()
17554 ) {
17555 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
17556 $this->get_plugin_name(),
17557 $this->get_plugin_name(),
17558 'manage_options',
17559 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
17560 );
17561 }
17562 } else {
17563 $this->_menu->add_subpage_and_update(
17564 $this->_menu->get_parent_slug(),
17565 $this->get_plugin_name(),
17566 $this->get_plugin_name(),
17567 'manage_options',
17568 $this->_menu->get_slug()
17569 );
17570 }
17571 }
17572
17573 /**
17574 * @author Leo Fajardo (@leorw)
17575 * @since 1.2.1
17576 *
17577 * return string
17578 */
17579 function get_top_level_menu_capability() {
17580 global $menu;
17581
17582 $top_level_menu_slug = $this->get_top_level_menu_slug();
17583
17584 foreach ( $menu as $menu_info ) {
17585 /**
17586 * The second element in the menu info array is the capability/role that has access to the menu and the
17587 * third element is the menu slug.
17588 */
17589 if ( $menu_info[2] === $top_level_menu_slug ) {
17590 return $menu_info[1];
17591 }
17592 }
17593
17594 return 'read';
17595 }
17596
17597 /**
17598 * @author Vova Feldman (@svovaf)
17599 * @since 1.0.0
17600 *
17601 * @return string
17602 */
17603 private function get_top_level_menu_slug() {
17604 return ( $this->is_addon() ?
17605 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
17606 $this->_menu->get_top_level_menu_slug() );
17607 }
17608
17609 /**
17610 * @author Vova Feldman (@svovaf)
17611 * @since 1.2.2.7
17612 *
17613 * @return string
17614 */
17615 function get_pricing_cta_label() {
17616 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
17617
17618 if ( $this->is_in_trial_promotion() &&
17619 ! $this->is_paying_or_trial()
17620 ) {
17621 // If running a trial promotion, modify the pricing to load the trial.
17622 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
17623 } else if ( $this->is_paying() ) {
17624 $label = $this->get_text_inline( 'Pricing', 'pricing' );
17625 }
17626
17627 return $label;
17628 }
17629
17630 /**
17631 * @author Vova Feldman (@svovaf)
17632 * @since 1.2.2.7
17633 *
17634 * @return bool
17635 */
17636 function is_pricing_page_visible() {
17637 return (
17638 // Has at least one paid plan.
17639 $this->has_paid_plan() &&
17640 // Didn't ask to hide the pricing page.
17641 $this->is_page_visible( 'pricing' ) &&
17642 // Don't have a valid active license or has more than one plan.
17643 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
17644 );
17645 }
17646
17647 /**
17648 * @author Leo Fajardo (@leorw)
17649 * @since 2.3.0
17650 *
17651 * @param bool $is_activation_mode
17652 *
17653 * @return bool
17654 */
17655 private function should_add_submenu_or_action_links( $is_activation_mode ) {
17656 if ( $this->is_addon() ) {
17657 // No submenu items or action links for add-ons.
17658 return false;
17659 }
17660
17661 if ( $this->show_opt_in_on_themes_page() ) {
17662 if ( ! fs_is_network_admin() ) {
17663 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
17664 return true;
17665 }
17666 } else if ( $is_activation_mode ) {
17667 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
17668 return false;
17669 }
17670
17671 if ( fs_is_network_admin() ) {
17672 /**
17673 * Add submenu items or action links to network level when plugin was network activated and the super
17674 * admin did NOT delegate the connection of all sites to site admins.
17675 */
17676 return (
17677 $this->_is_network_active &&
17678 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
17679 ! $this->is_network_delegated_connection() )
17680 );
17681 }
17682
17683 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
17684 }
17685
17686 /**
17687 * Add default Freemius menu items.
17688 *
17689 * @author Vova Feldman (@svovaf)
17690 * @since 1.0.0
17691 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
17692 */
17693 private function add_submenu_items() {
17694 $this->_logger->entrance();
17695
17696 $is_activation_mode = $this->is_activation_mode();
17697
17698 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
17699
17700 if ( $add_submenu_items ) {
17701 if ( $this->has_affiliate_program() ) {
17702 // Add affiliation page.
17703 $this->add_submenu_item(
17704 $this->get_text_inline( 'Affiliation', 'affiliation' ),
17705 array( &$this, '_affiliation_page_render' ),
17706 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
17707 'manage_options',
17708 'affiliation',
17709 'Freemius::_clean_admin_content_section',
17710 WP_FS__DEFAULT_PRIORITY,
17711 $this->is_submenu_item_visible( 'affiliation' )
17712 );
17713 }
17714 }
17715
17716 if ( $add_submenu_items ||
17717 ( $is_activation_mode &&
17718 $this->is_only_premium() &&
17719 $this->is_admin_page( 'account' ) &&
17720 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
17721 )
17722 ) {
17723 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
17724 $show_account = (
17725 $this->is_submenu_item_visible( 'account' ) &&
17726 /**
17727 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
17728 */
17729 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
17730 );
17731
17732 // Add user account page.
17733 $this->add_submenu_item(
17734 $this->get_text_inline( 'Account', 'account' ),
17735 array( &$this, '_account_page_render' ),
17736 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
17737 'manage_options',
17738 'account',
17739 array( &$this, '_account_page_load' ),
17740 WP_FS__DEFAULT_PRIORITY,
17741 ( $add_submenu_items && $show_account )
17742 );
17743 }
17744 }
17745
17746 if ( $add_submenu_items ) {
17747 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
17748 // Add contact page.
17749 $this->add_submenu_item(
17750 $this->get_text_inline( 'Contact Us', 'contact-us' ),
17751 array( &$this, '_contact_page_render' ),
17752 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
17753 'manage_options',
17754 'contact',
17755 'Freemius::_clean_admin_content_section',
17756 WP_FS__DEFAULT_PRIORITY,
17757 $this->is_submenu_item_visible( 'contact' )
17758 );
17759 }
17760
17761 if ( $this->has_addons() ) {
17762 $this->add_submenu_item(
17763 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
17764 array( &$this, '_addons_page_render' ),
17765 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
17766 'manage_options',
17767 'addons',
17768 array( &$this, '_addons_page_load' ),
17769 WP_FS__LOWEST_PRIORITY - 1,
17770 $this->is_submenu_item_visible( 'addons' )
17771 );
17772 }
17773 }
17774
17775 if ( $add_submenu_items ||
17776 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
17777 ) {
17778 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
17779 $show_pricing = (
17780 $this->is_submenu_item_visible( 'pricing' ) &&
17781 $this->is_pricing_page_visible()
17782 );
17783
17784 $pricing_cta_text = $this->get_pricing_cta_label();
17785 $pricing_class = 'upgrade-mode';
17786 if ( $show_pricing ) {
17787 if ( $this->is_in_trial_promotion() &&
17788 ! $this->is_paying_or_trial()
17789 ) {
17790 // If running a trial promotion, modify the pricing to load the trial.
17791 $pricing_class = 'trial-mode';
17792 } else if ( $this->is_paying() ) {
17793 $pricing_class = '';
17794 }
17795 }
17796
17797 // Add upgrade/pricing page.
17798 $this->add_submenu_item(
17799 $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' ) ),
17800 array( &$this, '_pricing_page_render' ),
17801 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
17802 'manage_options',
17803 'pricing',
17804 'Freemius::_clean_admin_content_section',
17805 WP_FS__LOWEST_PRIORITY,
17806 ( $add_submenu_items && $show_pricing ),
17807 $pricing_class
17808 );
17809 }
17810 }
17811
17812 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
17813 /**
17814 * Add the other menu items if there are any when not in activation mode or license activation is not
17815 * required (license activation is required for registered or anonymous users after activating the
17816 * premium version when the site is not in trial mode or there's no active valid license).
17817 *
17818 * @author Leo Fajardo (@leorw)
17819 * @since 2.2.1
17820 */
17821 if ( 0 < count( $this->_menu_items ) ) {
17822 if ( ! $this->_menu->is_top_level() ) {
17823 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
17824
17825 // Append submenu items right after the plugin's submenu item.
17826 $this->order_sub_submenu_items();
17827 } else {
17828 // Append submenu items.
17829 $this->embed_submenu_items();
17830 }
17831 }
17832 }
17833 }
17834
17835 /**
17836 * Moved the actual submenu item additions to a separated function,
17837 * in order to support sub-submenu items when the plugin's settings
17838 * only have a submenu and not top-level menu item.
17839 *
17840 * @author Vova Feldman (@svovaf)
17841 * @since 1.1.4
17842 */
17843 private function embed_submenu_items() {
17844 $item_template = $this->_menu->is_top_level() ?
17845 '<span class="fs-submenu-item %s %s %s">%s</span>' :
17846 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
17847
17848 $top_level_menu_capability = $this->get_top_level_menu_capability();
17849
17850 ksort( $this->_menu_items );
17851
17852 $is_first_submenu_item = true;
17853
17854 foreach ( $this->_menu_items as $priority => $items ) {
17855 foreach ( $items as $item ) {
17856 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
17857
17858 $menu_item = sprintf(
17859 $item_template,
17860 $this->get_unique_affix(),
17861 $item['menu_slug'],
17862 ! empty( $item['class'] ) ? $item['class'] : '',
17863 $item['menu_title']
17864 );
17865
17866 $top_level_menu_slug = $this->get_top_level_menu_slug();
17867 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
17868
17869 if ( ! isset( $item['url'] ) ) {
17870 $hook = FS_Admin_Menu_Manager::add_subpage(
17871 $item['show_submenu'] ?
17872 $top_level_menu_slug :
17873 null,
17874 $item['page_title'],
17875 $menu_item,
17876 $capability,
17877 $menu_slug,
17878 $item['render_function']
17879 );
17880
17881 if ( false !== $item['before_render_function'] ) {
17882 add_action( "load-$hook", $item['before_render_function'] );
17883 }
17884 } else {
17885 FS_Admin_Menu_Manager::add_subpage(
17886 $item['show_submenu'] ?
17887 $top_level_menu_slug :
17888 null,
17889 $item['page_title'],
17890 $menu_item,
17891 $capability,
17892 $menu_slug,
17893 array( $this, '' )
17894 );
17895 }
17896
17897 if ( $item['show_submenu'] && $is_first_submenu_item ) {
17898 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
17899 /**
17900 * If the top-level menu has been dynamically created, remove the first submenu item that
17901 * WordPress automatically creates when there's no submenu item whose slug matches the
17902 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
17903 *
17904 * Awesome Plugin
17905 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
17906 *
17907 * @author Leo Fajardo (@leorw)
17908 */
17909 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
17910 }
17911
17912 $is_first_submenu_item = false;
17913 }
17914 }
17915 }
17916 }
17917
17918 /**
17919 * Re-order the submenu items so all Freemius added new submenu items
17920 * are added right after the plugin's settings submenu item.
17921 *
17922 * @author Vova Feldman (@svovaf)
17923 * @since 1.1.4
17924 */
17925 private function order_sub_submenu_items() {
17926 global $submenu;
17927
17928 $menu_slug = $this->_menu->get_top_level_menu_slug();
17929
17930 /**
17931 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
17932 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
17933 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
17934 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
17935 *
17936 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
17937 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
17938 * capability needed to access the parent menu as the capability for the submenus that we will add.
17939 */
17940 if ( empty( $submenu[ $menu_slug ] ) ) {
17941 return;
17942 }
17943
17944 $top_level_menu = &$submenu[ $menu_slug ];
17945
17946 $all_submenu_items_after = array();
17947
17948 $found_submenu_item = false;
17949
17950 foreach ( $top_level_menu as $submenu_id => $meta ) {
17951 if ( $found_submenu_item ) {
17952 // Remove all submenu items after the plugin's submenu item.
17953 $all_submenu_items_after[] = $meta;
17954 unset( $top_level_menu[ $submenu_id ] );
17955 }
17956
17957 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
17958 // Found the submenu item, put all below.
17959 $found_submenu_item = true;
17960 continue;
17961 }
17962 }
17963
17964 // Embed all plugin's new submenu items.
17965 $this->embed_submenu_items();
17966
17967 // Start with specially high number to make sure it's appended.
17968 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
17969 foreach ( $all_submenu_items_after as $meta ) {
17970 $top_level_menu[ $i ] = $meta;
17971 $i ++;
17972 }
17973
17974 // Sort submenu items.
17975 ksort( $top_level_menu );
17976 }
17977
17978 /**
17979 * Helper method to return the module's support forum URL.
17980 *
17981 * @author Vova Feldman (@svovaf)
17982 * @since 1.2.2.7
17983 *
17984 * @return string
17985 */
17986 function get_support_forum_url() {
17987 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
17988 }
17989
17990 /**
17991 * Displays the Support Forum link when enabled.
17992 *
17993 * Can be filtered like so:
17994 *
17995 * function _fs_show_support_menu( $is_visible, $menu_id ) {
17996 * if ( 'support' === $menu_id ) {
17997 * return _fs->is_registered();
17998 * }
17999 * return $is_visible;
18000 * }
18001 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
18002 *
18003 */
18004 function _add_default_submenu_items() {
18005 if ( ! $this->is_on() ) {
18006 return;
18007 }
18008
18009 if ( ! $this->is_activation_mode() &&
18010 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
18011 ( ! $this->_is_network_active && is_admin() ) )
18012 ) {
18013 $this->add_submenu_link_item(
18014 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
18015 $this->get_support_forum_url(),
18016 'wp-support-forum',
18017 null,
18018 50,
18019 $this->is_submenu_item_visible( 'support' )
18020 );
18021 }
18022 }
18023
18024 /**
18025 * @author Vova Feldman (@svovaf)
18026 * @since 1.0.1
18027 *
18028 * @param string $menu_title
18029 * @param callable $render_function
18030 * @param bool|string $page_title
18031 * @param string $capability
18032 * @param bool|string $menu_slug
18033 * @param bool|callable $before_render_function
18034 * @param int $priority
18035 * @param bool $show_submenu
18036 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
18037 */
18038 function add_submenu_item(
18039 $menu_title,
18040 $render_function,
18041 $page_title = false,
18042 $capability = 'manage_options',
18043 $menu_slug = false,
18044 $before_render_function = false,
18045 $priority = WP_FS__DEFAULT_PRIORITY,
18046 $show_submenu = true,
18047 $class = ''
18048 ) {
18049 $this->_logger->entrance( 'Title = ' . $menu_title );
18050
18051 if ( $this->is_addon() ) {
18052 $parent_fs = $this->get_parent_instance();
18053
18054 if ( is_object( $parent_fs ) ) {
18055 $parent_fs->add_submenu_item(
18056 $menu_title,
18057 $render_function,
18058 $page_title,
18059 $capability,
18060 $menu_slug,
18061 $before_render_function,
18062 $priority,
18063 $show_submenu,
18064 $class
18065 );
18066
18067 return;
18068 }
18069 }
18070
18071 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
18072 $this->_menu_items[ $priority ] = array();
18073 }
18074
18075 $this->_menu_items[ $priority ][] = array(
18076 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
18077 'menu_title' => $menu_title,
18078 'capability' => $capability,
18079 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
18080 'render_function' => $render_function,
18081 'before_render_function' => $before_render_function,
18082 'show_submenu' => $show_submenu,
18083 'class' => $class,
18084 );
18085 }
18086
18087 /**
18088 * @author Vova Feldman (@svovaf)
18089 * @since 1.0.1
18090 *
18091 * @param string $menu_title
18092 * @param string $url
18093 * @param bool $menu_slug
18094 * @param string $capability
18095 * @param int $priority
18096 * @param bool $show_submenu
18097 */
18098 function add_submenu_link_item(
18099 $menu_title,
18100 $url,
18101 $menu_slug = false,
18102 $capability = 'read',
18103 $priority = WP_FS__DEFAULT_PRIORITY,
18104 $show_submenu = true
18105 ) {
18106 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
18107
18108 if ( $this->is_addon() ) {
18109 $parent_fs = $this->get_parent_instance();
18110
18111 if ( is_object( $parent_fs ) ) {
18112 $parent_fs->add_submenu_link_item(
18113 $menu_title,
18114 $url,
18115 $menu_slug,
18116 $capability,
18117 $priority,
18118 $show_submenu
18119 );
18120
18121 return;
18122 }
18123 }
18124
18125 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
18126 $this->_menu_items[ $priority ] = array();
18127 }
18128
18129 $this->_menu_items[ $priority ][] = array(
18130 'menu_title' => $menu_title,
18131 'capability' => $capability,
18132 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
18133 'url' => $url,
18134 'page_title' => $menu_title,
18135 'render_function' => 'fs_dummy',
18136 'before_render_function' => '',
18137 'show_submenu' => $show_submenu,
18138 );
18139 }
18140
18141 #endregion ------------------------------------------------------------------
18142
18143 #--------------------------------------------------------------------------------
18144 #region Admin Notices
18145 #--------------------------------------------------------------------------------
18146
18147 /**
18148 * @author Vova Feldman (@svovaf)
18149 * @since 2.3.1
18150 *
18151 * @param string|string[] $ids
18152 * @param int|null $network_level_or_blog_id
18153 *
18154 * @uses FS_Admin_Notices::remove_sticky()
18155 */
18156 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
18157 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
18158 }
18159
18160 #endregion
18161
18162 #--------------------------------------------------------------------------------
18163 #region Actions / Hooks / Filters
18164 #--------------------------------------------------------------------------------
18165
18166 /**
18167 * @author Vova Feldman (@svovaf)
18168 * @since 1.1.7
18169 *
18170 * @param string $tag
18171 *
18172 * @return string
18173 */
18174 public function get_action_tag( $tag ) {
18175 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
18176 }
18177
18178 /**
18179 * @author Vova Feldman (@svovaf)
18180 * @since 1.2.1.6
18181 *
18182 * @param string $tag
18183 * @param string $slug
18184 * @param bool $is_plugin
18185 *
18186 * @return string
18187 */
18188 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
18189 $action = "fs_{$tag}";
18190
18191 if ( ! empty( $slug ) ) {
18192 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
18193 }
18194
18195 return $action;
18196 }
18197
18198 /**
18199 * Returns a string that can be used to generate a unique action name,
18200 * option name, HTML element ID, or HTML element class.
18201 *
18202 * @author Leo Fajardo (@leorw)
18203 * @since 1.2.2
18204 *
18205 * @return string
18206 */
18207 public function get_unique_affix() {
18208 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
18209 }
18210
18211 /**
18212 * Returns a string that can be used to generate a unique action name,
18213 * option name, HTML element ID, or HTML element class.
18214 *
18215 * @author Vova Feldman (@svovaf)
18216 * @since 1.2.2.5
18217 *
18218 * @param string $slug
18219 * @param bool $is_plugin
18220 *
18221 * @return string
18222 */
18223 static function get_module_unique_affix( $slug, $is_plugin = true ) {
18224 $affix = $slug;
18225
18226 if ( ! $is_plugin ) {
18227 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
18228 }
18229
18230 return $affix;
18231 }
18232
18233 /**
18234 * @author Vova Feldman (@svovaf)
18235 * @since 1.2.1
18236 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
18237 * based on the slug for backward compatibility.
18238 *
18239 * @param string $tag
18240 *
18241 * @return string
18242 */
18243 function get_ajax_action( $tag ) {
18244 return self::get_ajax_action_static( $tag, $this->_module_id );
18245 }
18246
18247 /**
18248 * @author Vova Feldman (@svovaf)
18249 * @since 1.2.1.7
18250 *
18251 * @param string $tag
18252 *
18253 * @return string
18254 */
18255 function get_ajax_security( $tag ) {
18256 return wp_create_nonce( $this->get_ajax_action( $tag ) );
18257 }
18258
18259 /**
18260 * @author Vova Feldman (@svovaf)
18261 * @since 1.2.1.7
18262 *
18263 * @param string $tag
18264 */
18265 function check_ajax_referer( $tag ) {
18266 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
18267 }
18268
18269 /**
18270 * @author Vova Feldman (@svovaf)
18271 * @since 1.2.1.6
18272 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
18273 * based on the slug for backward compatibility.
18274 *
18275 * @param string $tag
18276 * @param number|null $module_id
18277 *
18278 * @return string
18279 */
18280 private static function get_ajax_action_static( $tag, $module_id = null ) {
18281 $action = "fs_{$tag}";
18282
18283 if ( ! empty( $module_id ) ) {
18284 $action .= "_{$module_id}";
18285 }
18286
18287 return $action;
18288 }
18289
18290 /**
18291 * Do action, specific for the current context plugin.
18292 *
18293 * @author Vova Feldman (@svovaf)
18294 * @since 1.0.1
18295 *
18296 * @param string $tag The name of the action to be executed.
18297 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
18298 * functions hooked to the action. Default empty.
18299 *
18300 * @uses do_action()
18301 */
18302 function do_action( $tag, $arg = '' ) {
18303 $this->_logger->entrance( $tag );
18304
18305 $args = func_get_args();
18306
18307 call_user_func_array( 'do_action', array_merge(
18308 array( $this->get_action_tag( $tag ) ),
18309 array_slice( $args, 1 ) )
18310 );
18311 }
18312
18313 /**
18314 * Add action, specific for the current context plugin.
18315 *
18316 * @author Vova Feldman (@svovaf)
18317 * @since 1.0.1
18318 *
18319 * @param string $tag
18320 * @param callable $function_to_add
18321 * @param int $priority
18322 * @param int $accepted_args
18323 *
18324 * @uses add_action()
18325 */
18326 function add_action(
18327 $tag,
18328 $function_to_add,
18329 $priority = WP_FS__DEFAULT_PRIORITY,
18330 $accepted_args = 1
18331 ) {
18332 $this->_logger->entrance( $tag );
18333
18334 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
18335 }
18336
18337 /**
18338 * Add AJAX action, specific for the current context plugin.
18339 *
18340 * @author Vova Feldman (@svovaf)
18341 * @since 1.2.1
18342 *
18343 * @param string $tag
18344 * @param callable $function_to_add
18345 * @param int $priority
18346 *
18347 * @uses add_action()
18348 *
18349 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
18350 */
18351 function add_ajax_action(
18352 $tag,
18353 $function_to_add,
18354 $priority = WP_FS__DEFAULT_PRIORITY
18355 ) {
18356 $this->_logger->entrance( $tag );
18357
18358 return self::add_ajax_action_static(
18359 $tag,
18360 $function_to_add,
18361 $priority,
18362 $this->_module_id
18363 );
18364 }
18365
18366 /**
18367 * Add AJAX action.
18368 *
18369 * @author Vova Feldman (@svovaf)
18370 * @since 1.2.1.6
18371 *
18372 * @param string $tag
18373 * @param callable $function_to_add
18374 * @param int $priority
18375 * @param number|null $module_id
18376 *
18377 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
18378 * @uses add_action()
18379 *
18380 */
18381 static function add_ajax_action_static(
18382 $tag,
18383 $function_to_add,
18384 $priority = WP_FS__DEFAULT_PRIORITY,
18385 $module_id = null
18386 ) {
18387 self::$_static_logger->entrance( $tag );
18388
18389 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
18390 return false;
18391 }
18392
18393 add_action(
18394 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
18395 $function_to_add,
18396 $priority,
18397 0
18398 );
18399
18400 self::$_static_logger->info( "$tag AJAX callback action added." );
18401
18402 return true;
18403 }
18404
18405 /**
18406 * Send a JSON response back to an Ajax request.
18407 *
18408 * @author Vova Feldman (@svovaf)
18409 * @since 1.2.1.5
18410 *
18411 * @param mixed $response
18412 */
18413 static function shoot_ajax_response( $response ) {
18414 wp_send_json( $response );
18415 }
18416
18417 /**
18418 * Send a JSON response back to an Ajax request, indicating success.
18419 *
18420 * @author Vova Feldman (@svovaf)
18421 * @since 1.2.1.5
18422 *
18423 * @param mixed $data Data to encode as JSON, then print and exit.
18424 */
18425 static function shoot_ajax_success( $data = null ) {
18426 wp_send_json_success( $data );
18427 }
18428
18429 /**
18430 * Send a JSON response back to an Ajax request, indicating failure.
18431 *
18432 * @author Vova Feldman (@svovaf)
18433 * @since 1.2.1.5
18434 *
18435 * @param mixed $error Optional error message.
18436 */
18437 static function shoot_ajax_failure( $error = '' ) {
18438 $result = array( 'success' => false );
18439 if ( ! empty( $error ) ) {
18440 $result['error'] = $error;
18441 }
18442
18443 wp_send_json( $result );
18444 }
18445
18446 /**
18447 * Apply filter, specific for the current context plugin.
18448 *
18449 * @author Vova Feldman (@svovaf)
18450 * @since 1.0.9
18451 *
18452 * @param string $tag The name of the filter hook.
18453 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
18454 *
18455 * @return mixed The filtered value after all hooked functions are applied to it.
18456 *
18457 * @uses apply_filters()
18458 */
18459 function apply_filters( $tag, $value ) {
18460 $this->_logger->entrance( $tag );
18461
18462 $args = func_get_args();
18463 array_unshift( $args, $this->get_unique_affix() );
18464
18465 return call_user_func_array( 'fs_apply_filter', $args );
18466 }
18467
18468 /**
18469 * Add filter, specific for the current context plugin.
18470 *
18471 * @author Vova Feldman (@svovaf)
18472 * @since 1.0.9
18473 *
18474 * @param string $tag
18475 * @param callable $function_to_add
18476 * @param int $priority
18477 * @param int $accepted_args
18478 *
18479 * @uses add_filter()
18480 */
18481 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
18482 $this->_logger->entrance( $tag );
18483
18484 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
18485 }
18486
18487 /**
18488 * Check if has filter.
18489 *
18490 * @author Vova Feldman (@svovaf)
18491 * @since 1.1.4
18492 *
18493 * @param string $tag
18494 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
18495 *
18496 * @return false|int
18497 *
18498 * @uses has_filter()
18499 */
18500 function has_filter( $tag, $function_to_check = false ) {
18501 $this->_logger->entrance( $tag );
18502
18503 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
18504 }
18505
18506 #endregion
18507
18508 /**
18509 * Override default i18n text phrases.
18510 *
18511 * @author Vova Feldman (@svovaf)
18512 * @since 1.1.6
18513 *
18514 * @param string[] string $key_value
18515 *
18516 * @uses fs_override_i18n()
18517 */
18518 function override_i18n( $key_value ) {
18519 fs_override_i18n( $key_value, $this->_slug );
18520 }
18521
18522 /* Account Page
18523 ------------------------------------------------------------------------------------------------------------------*/
18524 /**
18525 * Update site information.
18526 *
18527 * @author Vova Feldman (@svovaf)
18528 * @since 1.0.1
18529 *
18530 * @param bool $store Flush to Database if true.
18531 * @param null|int $network_level_or_blog_id Since 2.0.0
18532 * @param \FS_Site $site Since 2.0.0
18533 */
18534 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null ) {
18535 $this->_logger->entrance();
18536
18537 if ( is_null( $site ) ) {
18538 $site = $this->_site;
18539 }
18540
18541 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
18542 $this->_logger->error( "Empty install ID, can't store site." );
18543
18544 return;
18545 }
18546
18547 $site_clone = clone $site;
18548
18549 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id );
18550
18551 if ( is_object( $this->_user ) && $this->_user->id != $site->user_id ) {
18552 $this->sync_user_by_current_install( $site->user_id );
18553
18554 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
18555
18556 if ( empty( $prev_stored_user_id ) &&
18557 is_object($this->_user) && $this->_user->id != $site->user_id
18558 ) {
18559 /**
18560 * Store the current user ID as the previous user ID so that the previous user can be used
18561 * as the install's owner while the new owner's details are not yet available.
18562 *
18563 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
18564 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
18565 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
18566 * into `replica`.
18567 *
18568 * @author Leo Fajardo (@leorw)
18569 */
18570 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
18571 }
18572 }
18573
18574 $sites[ $this->_slug ] = $site_clone;
18575
18576 $this->set_account_option( 'sites', $sites, $store, $network_level_or_blog_id );
18577 }
18578
18579 /**
18580 * Update plugin's plans information.
18581 *
18582 * @author Vova Feldman (@svovaf)
18583 * @since 1.0.2
18584 *
18585 * @param bool $store Flush to Database if true.
18586 */
18587 private function _store_plans( $store = true ) {
18588 $this->_logger->entrance();
18589
18590 $plans = self::get_all_plans( $this->_module_type );
18591
18592 // Copy plans.
18593 $encrypted_plans = array();
18594 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
18595 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
18596 }
18597
18598 $plans[ $this->_slug ] = $encrypted_plans;
18599
18600 $this->set_account_option( 'plans', $plans, $store );
18601 }
18602
18603 /**
18604 * Update user's plugin licenses.
18605 *
18606 * @author Vova Feldman (@svovaf)
18607 * @since 1.0.5
18608 *
18609 * @param bool $store
18610 * @param number|bool $module_id
18611 * @param FS_Plugin_License[] $licenses
18612 */
18613 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
18614 $this->_logger->entrance();
18615
18616 $all_licenses = self::get_all_licenses();
18617
18618 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
18619 $module_id = $this->_module_id;
18620
18621 $user_licenses = is_array( $this->_licenses ) ?
18622 $this->_licenses :
18623 array();
18624
18625 if ( empty( $user_licenses ) ) {
18626 // If the context user doesn't have any license, don't update the licenses collection.
18627 return;
18628 }
18629
18630 $new_user_licenses_map = array();
18631 foreach ( $user_licenses as $user_license ) {
18632 $new_user_licenses_map[ $user_license->id ] = $user_license;
18633 }
18634
18635 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
18636
18637 // Update user licenses.
18638 $licenses_to_update_count = count( $new_user_licenses_map );
18639 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
18640 if ( 0 === $licenses_to_update_count ) {
18641 break;
18642 }
18643
18644 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
18645 // Update license.
18646 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
18647 unset( $new_user_licenses_map[ $license->id ] );
18648
18649 $licenses_to_update_count --;
18650 }
18651 }
18652
18653 if ( ! empty( $new_user_licenses_map ) ) {
18654 // Add new licenses.
18655 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
18656 }
18657
18658 $licenses = $all_licenses[ $module_id ];
18659 }
18660
18661 if ( ! isset( $all_licenses[ $module_id ] ) ) {
18662 $all_licenses[ $module_id ] = array();
18663 }
18664
18665 $all_licenses[ $module_id ] = $licenses;
18666
18667 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
18668 }
18669
18670 /**
18671 * Update user information.
18672 *
18673 * @author Vova Feldman (@svovaf)
18674 * @since 1.0.1
18675 *
18676 * @param bool $store Flush to Database if true.
18677 */
18678 private function _store_user( $store = true ) {
18679 $this->_logger->entrance();
18680
18681 if ( empty( $this->_user->id ) ) {
18682 $this->_logger->error( "Empty user ID, can't store user." );
18683
18684 return;
18685 }
18686
18687 $users = self::get_all_users();
18688 $users[ $this->_user->id ] = $this->_user;
18689 self::$_accounts->set_option( 'users', $users, $store );
18690 }
18691
18692 /**
18693 * Update new updates information.
18694 *
18695 * @author Vova Feldman (@svovaf)
18696 * @since 1.0.4
18697 *
18698 * @param FS_Plugin_Tag|null $update
18699 * @param bool $store Flush to Database if true.
18700 * @param bool|number $plugin_id
18701 */
18702 private function _store_update( $update, $store = true, $plugin_id = false ) {
18703 $this->_logger->entrance();
18704
18705 if ( $update instanceof FS_Plugin_Tag ) {
18706 $update->updated = time();
18707 }
18708
18709 if ( ! is_numeric( $plugin_id ) ) {
18710 $plugin_id = $this->_plugin->id;
18711 }
18712
18713 $updates = self::get_all_updates();
18714 $updates[ $plugin_id ] = $update;
18715 self::$_accounts->set_option( 'updates', $updates, $store );
18716 }
18717
18718 /**
18719 * Update new updates information.
18720 *
18721 * @author Vova Feldman (@svovaf)
18722 * @since 1.0.6
18723 *
18724 * @param FS_Plugin[] $plugin_addons
18725 * @param bool $store Flush to Database if true.
18726 */
18727 private function _store_addons( $plugin_addons, $store = true ) {
18728 $this->_logger->entrance();
18729
18730 $addons = self::get_all_addons();
18731 $addons[ $this->_plugin->id ] = $plugin_addons;
18732 self::$_accounts->set_option( 'addons', $addons, $store );
18733 }
18734
18735 /**
18736 * Delete plugin's associated add-ons.
18737 *
18738 * @author Vova Feldman (@svovaf)
18739 * @since 1.0.8
18740 *
18741 * @param bool $store
18742 *
18743 * @return bool
18744 */
18745 private function _delete_account_addons( $store = true ) {
18746 $all_addons = self::get_all_account_addons();
18747
18748 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
18749 return false;
18750 }
18751
18752 unset( $all_addons[ $this->_plugin->id ] );
18753
18754 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
18755
18756 return true;
18757 }
18758
18759 /**
18760 * Update account add-ons list.
18761 *
18762 * @author Vova Feldman (@svovaf)
18763 * @since 1.0.6
18764 *
18765 * @param FS_Plugin[] $addons
18766 * @param bool $store Flush to Database if true.
18767 */
18768 private function _store_account_addons( $addons, $store = true ) {
18769 $this->_logger->entrance();
18770
18771 $all_addons = self::get_all_account_addons();
18772 $all_addons[ $this->_plugin->id ] = $addons;
18773 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
18774 }
18775
18776 /**
18777 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
18778 * the valid user licenses will be fetched again and the account add-ons may be updated.
18779 *
18780 * @author Leo Fajardo (@leorw)
18781 * @since 2.2.4
18782 */
18783 private function purge_valid_user_licenses_cache() {
18784 if ( ! $this->is_registered() ) {
18785 return;
18786 }
18787
18788 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
18789 }
18790
18791 /**
18792 * @author Leo Fajardo (@leorw)
18793 * @since 2.3.0
18794 *
18795 * @param array $all_licenses
18796 * @param number|null $site_license_id
18797 * @param bool $include_parent_licenses
18798 *
18799 * @return array
18800 */
18801 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
18802 $foreign_licenses = array(
18803 'ids' => array(),
18804 'license_keys' => array()
18805 );
18806
18807 $parent_license_ids_map = array();
18808
18809 foreach ( $all_licenses as $license ) {
18810 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
18811 continue;
18812 }
18813
18814 $foreign_licenses['ids'][] = $license->id;
18815 $foreign_licenses['license_keys'][] = $license->secret_key;
18816
18817 if (
18818 $include_parent_licenses &&
18819 is_object( $this->_license ) &&
18820 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
18821 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
18822 ) {
18823 /**
18824 * Include the parent license's info only if it has not been included before since child licenses
18825 * can have the same parent license.
18826 */
18827 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
18828 $foreign_licenses['license_keys'][] = $license->secret_key;
18829
18830 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
18831 }
18832 }
18833
18834 if ( empty( $foreign_licenses['ids'] ) ) {
18835 $foreign_licenses = array();
18836 }
18837
18838 return $foreign_licenses;
18839 }
18840
18841 /**
18842 * @author Leo Fajardo (@leorw)
18843 * @since 2.3.0
18844 *
18845 * @return string
18846 */
18847 private function get_valid_user_licenses_endpoint() {
18848 $user_licenses_endpoint = '/licenses.json?type=active' .
18849 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
18850
18851 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
18852
18853 if ( ! empty ( $foreign_licenses ) ) {
18854 $foreign_licenses = array(
18855 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
18856 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
18857 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
18858 );
18859
18860 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
18861 }
18862
18863 return $user_licenses_endpoint;
18864 }
18865
18866 /**
18867 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
18868 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
18869 * method filters out non–add-on product IDs and stores the add-on IDs.
18870 *
18871 * @author Leo Fajardo (@leorw)
18872 * @since 2.2.4
18873 *
18874 * @return stdClass[] array
18875 */
18876 private function fetch_valid_user_licenses() {
18877 $this->_logger->entrance();
18878
18879 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
18880
18881 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
18882 ! is_array( $result->licenses )
18883 ) {
18884 return array();
18885 }
18886
18887 return $result->licenses;
18888 }
18889
18890 /**
18891 * @author Leo Fajardo (@leorw)
18892 * @since 2.2.4
18893 *
18894 * @return number[] Account add-on IDs.
18895 */
18896 function get_updated_account_addons() {
18897 $addons = $this->get_addons();
18898 if ( empty( $addons ) ) {
18899 return array();
18900 }
18901
18902 $account_addons = $this->get_account_addons();
18903 if ( ! is_array( $account_addons ) ) {
18904 $account_addons = array();
18905 }
18906
18907 $user_licenses = $this->is_registered() ?
18908 $this->fetch_valid_user_licenses() :
18909 array();
18910
18911 if ( empty( $user_licenses ) ) {
18912 return $account_addons;
18913 }
18914
18915 $addon_ids = array();
18916 foreach ( $addons as $addon ) {
18917 $addon_ids[] = $addon->id;
18918 }
18919
18920 $license_product_ids = array();
18921
18922 foreach ( $user_licenses as $license ) {
18923 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
18924 $license_product_ids = array_merge( $license_product_ids, $license->products );
18925 } else {
18926 $license_product_ids[] = $license->plugin_id;
18927 }
18928 }
18929
18930 // Filter out non–add-on IDs.
18931 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
18932 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
18933 $this->_store_account_addons( array_unique( $new_account_addons ) );
18934 }
18935
18936 return $new_account_addons;
18937 }
18938
18939 /**
18940 * Store account params in the Database.
18941 *
18942 * @author Vova Feldman (@svovaf)
18943 * @since 1.0.1
18944 *
18945 * @param null|int $blog_id Since 2.0.0
18946 */
18947 private function _store_account( $blog_id = null ) {
18948 $this->_logger->entrance();
18949
18950 $this->_store_site( false, $blog_id );
18951 $this->_store_user( false );
18952 $this->_store_plans( false );
18953 $this->_store_licenses( false );
18954
18955 self::$_accounts->store( $blog_id );
18956 }
18957
18958 /**
18959 * Sync user's information.
18960 *
18961 * @author Vova Feldman (@svovaf)
18962 * @since 1.0.3
18963 * @uses FS_Api
18964 */
18965 private function _handle_account_user_sync() {
18966 $this->_logger->entrance();
18967
18968 $api = $this->get_api_user_scope();
18969
18970 // Get user's information.
18971 $user = $api->get( '/', true );
18972
18973 if ( isset( $user->id ) ) {
18974 $this->_user->first = $user->first;
18975 $this->_user->last = $user->last;
18976 $this->_user->email = $user->email;
18977
18978 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
18979
18980 if ( $user->is_verified &&
18981 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
18982 ) {
18983 $this->_user->is_verified = true;
18984
18985 $this->do_action( 'account_email_verified', $user->email );
18986
18987 $this->_admin_notices->add(
18988 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
18989 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
18990 'success',
18991 // Make admin sticky if account menu item is invisible,
18992 // since the page will be auto redirected to the plugin's
18993 // main settings page, and the non-sticky message
18994 // will disappear.
18995 ! $is_menu_item_account_visible,
18996 'email_verified'
18997 );
18998 }
18999
19000 // Flush user details to DB.
19001 $this->_store_user();
19002
19003 $this->do_action( 'after_account_user_sync', $user );
19004
19005 /**
19006 * If account menu item is hidden, redirect to plugin's main settings page.
19007 *
19008 * @author Vova Feldman (@svovaf)
19009 * @since 1.1.6
19010 *
19011 * @link https://github.com/Freemius/wordpress-sdk/issues/6
19012 */
19013 if ( ! $is_menu_item_account_visible ) {
19014 fs_redirect( $this->_get_admin_page_url() );
19015 }
19016 }
19017 }
19018
19019 /**
19020 * @author Vova Feldman (@svovaf)
19021 * @since 1.0.9
19022 * @uses FS_Api
19023 *
19024 * @param number|bool $license_id
19025 *
19026 * @return FS_Subscription|object|bool
19027 */
19028 private function _fetch_site_license_subscription( $license_id = false ) {
19029 $this->_logger->entrance();
19030 $api = $this->get_api_site_scope();
19031
19032 if ( ! is_numeric( $license_id ) ) {
19033 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
19034 $this->_license->parent_license_id :
19035 $this->_license->id;
19036 }
19037
19038 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
19039
19040 return ! isset( $result->error ) ?
19041 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
19042 new FS_Subscription( $result->subscriptions[0] ) :
19043 false
19044 ) :
19045 $result;
19046 }
19047
19048 /**
19049 * @author Vova Feldman (@svovaf)
19050 * @since 1.0.4
19051 * @uses FS_Api
19052 *
19053 * @param number|bool $plan_id
19054 *
19055 * @return FS_Plugin_Plan|object
19056 */
19057 private function _fetch_site_plan( $plan_id = false ) {
19058 $this->_logger->entrance();
19059 $api = $this->get_api_site_scope();
19060
19061 if ( ! is_numeric( $plan_id ) ) {
19062 $plan_id = $this->_site->plan_id;
19063 }
19064
19065 $plan = $api->get( "/plans/{$plan_id}.json", true );
19066
19067 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
19068 }
19069
19070 /**
19071 * @author Vova Feldman (@svovaf)
19072 * @since 1.0.5
19073 * @uses FS_Api
19074 *
19075 * @return FS_Plugin_Plan[]|object
19076 */
19077 private function _fetch_plugin_plans() {
19078 $this->_logger->entrance();
19079 $api = $this->get_current_or_network_user_api_scope();
19080
19081 /**
19082 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
19083 */
19084 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
19085
19086 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
19087 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
19088 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
19089 }
19090
19091 $result = $result->plans;
19092 }
19093
19094 return $result;
19095 }
19096
19097 /**
19098 * @author Vova Feldman (@svovaf)
19099 * @since 2.0.0
19100 *
19101 * @param number $plan_id
19102 *
19103 * @return \FS_Plugin_Plan|object
19104 */
19105 private function fetch_plan_by_id( $plan_id ) {
19106 $this->_logger->entrance();
19107 $api = $this->get_current_or_network_user_api_scope();
19108
19109 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
19110
19111 return $this->is_api_result_entity( $result ) ?
19112 new FS_Plugin_Plan( $result ) :
19113 $result;
19114 }
19115
19116 /**
19117 * @author Vova Feldman (@svovaf)
19118 * @since 1.0.5
19119 * @uses FS_Api
19120 *
19121 * @param number|bool $plugin_id
19122 * @param number|bool $site_license_id
19123 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
19124 * @param number|null $blog_id
19125 *
19126 * @return FS_Plugin_License[]|object
19127 */
19128 private function _fetch_licenses(
19129 $plugin_id = false,
19130 $site_license_id = false,
19131 $foreign_licenses = array(),
19132 $blog_id = null
19133 ) {
19134 $this->_logger->entrance();
19135
19136 $api = $this->get_api_user_scope();
19137
19138 if ( ! is_numeric( $plugin_id ) ) {
19139 $plugin_id = $this->_plugin->id;
19140 }
19141
19142 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
19143 if ( ! empty ( $foreign_licenses ) ) {
19144 $foreign_licenses = array(
19145 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
19146 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
19147 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
19148 );
19149
19150 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
19151 }
19152
19153 $result = $api->get( $user_licenses_endpoint, true );
19154
19155 $is_site_license_synced = false;
19156
19157 $api_errors = array();
19158
19159 if ( $this->is_api_result_object( $result, 'licenses' ) &&
19160 is_array( $result->licenses )
19161 ) {
19162 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
19163 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
19164
19165 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
19166 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
19167 }
19168 }
19169
19170 $result = $result->licenses;
19171 } else {
19172 $api_errors[] = $result;
19173 $result = array();
19174 }
19175
19176 if ( ! $is_site_license_synced ) {
19177 if ( ! is_null( $blog_id ) ) {
19178 /**
19179 * If blog ID is not null, the request is for syncing of the license of a single site via the
19180 * network-level "Account" page.
19181 *
19182 * @author Leo Fajardo (@leorw)
19183 */
19184 $this->switch_to_blog( $blog_id );
19185 }
19186
19187 $api = $this->get_api_site_scope();
19188
19189 if ( is_numeric( $site_license_id ) ) {
19190 // Try to retrieve a foreign license that is linked to the install.
19191 $api_result = $api->call( '/licenses.json?is_enriched=true' );
19192
19193 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
19194 is_array( $api_result->licenses )
19195 ) {
19196 $licenses = $api_result->licenses;
19197
19198 if ( ! empty( $licenses ) ) {
19199 $result[] = new FS_Plugin_License( $licenses[0] );
19200 }
19201 } else {
19202 $api_errors[] = $api_result;
19203 }
19204 } else if (
19205 is_object( $this->_license ) &&
19206 /**
19207 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
19208 * the FS instance that does the syncing is the parent FS instance.
19209 *
19210 * @author Leo Fajardo (@leorw)
19211 * @since 2.3.0
19212 */
19213 $this->_license->plugin_id == $plugin_id
19214 ) {
19215 $is_license_in_result = false;
19216 if ( ! empty( $result ) ) {
19217 foreach ( $result as $license ) {
19218 if ( $license->id == $this->_license->id ) {
19219 $is_license_in_result = true;
19220 break;
19221 }
19222 }
19223 }
19224
19225 if ( ! $is_license_in_result ) {
19226 // Fetch foreign license by ID and license key.
19227 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
19228 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
19229
19230 if ( $this->is_api_result_entity( $license ) ) {
19231 $result[] = new FS_Plugin_License( $license );
19232 } else {
19233 $api_errors[] = $license;
19234 }
19235 }
19236 }
19237
19238 if ( ! is_null( $blog_id ) ) {
19239 $this->switch_to_blog( $this->_storage->network_install_blog_id );
19240 }
19241 }
19242
19243 if ( is_array( $result ) && 0 < count( $result ) ) {
19244 // If found at least one license, return license collection even if there are errors.
19245 return $result;
19246 }
19247
19248 if ( ! empty( $api_errors ) ) {
19249 // If found any errors and no licenses, return first error.
19250 return $api_errors[0];
19251 }
19252
19253 // Fallback to empty licenses list.
19254 return $result;
19255 }
19256
19257 /**
19258 * @author Vova Feldman (@svovaf)
19259 * @since 2.0.0
19260 *
19261 * @param number $license_id
19262 * @param string $license_key
19263 *
19264 * @return \FS_Plugin_License|object
19265 */
19266 private function fetch_license_by_key( $license_id, $license_key ) {
19267 $this->_logger->entrance();
19268
19269 $api = $this->get_current_or_network_user_api_scope();
19270
19271 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
19272
19273 return $this->is_api_result_entity( $result ) ?
19274 new FS_Plugin_License( $result ) :
19275 $result;
19276 }
19277
19278 /**
19279 * @author Vova Feldman (@svovaf)
19280 * @since 1.2.0
19281 * @uses FS_Api
19282 *
19283 * @param number|bool $plugin_id
19284 * @param bool $flush
19285 *
19286 * @return FS_Payment[]|object
19287 */
19288 function _fetch_payments( $plugin_id = false, $flush = false ) {
19289 $this->_logger->entrance();
19290
19291 $api = $this->get_api_user_scope();
19292
19293 if ( ! is_numeric( $plugin_id ) ) {
19294 $plugin_id = $this->_plugin->id;
19295 }
19296
19297 $include_bundles = (
19298 is_object( $this->_plugin ) &&
19299 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
19300 );
19301
19302 $result = $api->get(
19303 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
19304 $flush
19305 );
19306
19307 if ( ! isset( $result->error ) ) {
19308 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
19309 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
19310 }
19311 $result = $result->payments;
19312 }
19313
19314 return $result;
19315 }
19316
19317 /**
19318 * @author Vova Feldman (@svovaf)
19319 * @since 1.2.1.5
19320 * @uses FS_Api
19321 *
19322 * @param bool $flush
19323 *
19324 * @return \FS_Billing|mixed
19325 */
19326 function _fetch_billing( $flush = false ) {
19327 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
19328
19329 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
19330
19331 if ( $this->is_api_result_entity( $billing ) ) {
19332 $billing = new FS_Billing( $billing );
19333 }
19334
19335 return $billing;
19336 }
19337
19338 /**
19339 * @author Vova Feldman (@svovaf)
19340 * @since 1.0.5
19341 *
19342 * @param FS_Plugin_License[] $licenses
19343 * @param number $module_id
19344 */
19345 private function _update_licenses( $licenses, $module_id ) {
19346 $this->_logger->entrance();
19347
19348 if ( is_array( $licenses ) ) {
19349 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
19350 $licenses[ $i ]->updated = time();
19351 }
19352 }
19353
19354 $this->_store_licenses( true, $module_id, $licenses );
19355 }
19356
19357 /**
19358 * @author Vova Feldman (@svovaf)
19359 * @since 1.0.4
19360 *
19361 * @param bool|number $plugin_id
19362 * @param bool $flush Since 1.1.7.3
19363 * @param int $expiration Since 1.2.2.7
19364 * @param bool|string $newer_than Since 2.2.1
19365 *
19366 * @return object|false New plugin tag info if exist.
19367 */
19368 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
19369 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
19370
19371 if ( ! is_object( $latest_tag ) ) {
19372 return false;
19373 }
19374
19375 $plugin_version = $this->get_plugin_version();
19376
19377 // Check if version is actually newer.
19378 $has_new_version =
19379 // If it's an non-installed add-on then always return latest.
19380 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
19381 // Compare versions.
19382 version_compare( $plugin_version, $latest_tag->version, '<' );
19383
19384 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
19385
19386 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
19387
19388 $this->_storage->beta_data = array(
19389 'is_beta' => $is_latest_version_beta,
19390 'version' => $latest_tag->version
19391 );
19392
19393 return $has_new_version ? $latest_tag : false;
19394 }
19395
19396 /**
19397 * @author Vova Feldman (@svovaf)
19398 * @since 1.0.5
19399 *
19400 * @param bool|number $plugin_id
19401 * @param bool $flush Since 1.1.7.3
19402 * @param int $expiration Since 1.2.2.7
19403 * @param bool|string $newer_than Since 2.2.1
19404 *
19405 * @return bool|FS_Plugin_Tag
19406 */
19407 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
19408 $this->_logger->entrance();
19409
19410 if ( ! is_numeric( $plugin_id ) ) {
19411 $plugin_id = $this->_plugin->id;
19412 }
19413
19414 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
19415 $updates = $this->get_all_updates();
19416
19417 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
19418 }
19419
19420 /**
19421 * Check if site assigned with active license.
19422 *
19423 * @author Vova Feldman (@svovaf)
19424 * @since 1.0.6
19425 *
19426 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
19427 */
19428 function has_active_license() {
19429 return (
19430 is_object( $this->_license ) &&
19431 is_numeric( $this->_license->id ) &&
19432 ! $this->_license->is_expired()
19433 );
19434 }
19435
19436 /**
19437 * Check if site assigned with active & valid (not expired) license.
19438 *
19439 * @author Vova Feldman (@svovaf)
19440 * @since 1.2.1
19441 */
19442 function has_active_valid_license() {
19443 return self::is_active_valid_license( $this->_license );
19444 }
19445
19446 /**
19447 * @author Leo Fajardo (@leorw)
19448 * @since 2.3.1
19449 */
19450 function is_data_debug_mode() {
19451 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
19452 return false;
19453 }
19454
19455 $fs = $this->is_addon() ?
19456 $this->get_parent_instance() :
19457 $this;
19458
19459 if ( $fs->is_network_active() && fs_is_network_admin() ) {
19460 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
19461 } else {
19462 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
19463 }
19464
19465 return ( 'true' === $is_developer_license_debug_mode );
19466 }
19467
19468 /**
19469 * @author Leo Fajardo (@leorw)
19470 * @since 2.3.1
19471 */
19472 function _set_data_debug_mode() {
19473 if ( ! $this->is_whitelabeled( true ) ) {
19474 return;
19475 }
19476
19477 $license_or_user_key = fs_request_get( 'license_or_user_key' );
19478
19479 $transient_value = ( ! empty( $license_or_user_key ) ) ?
19480 'true' :
19481 'false';
19482
19483 if ( 'true' === $transient_value ) {
19484 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
19485 'last_license_key' :
19486 'last_license_user_key'
19487 );
19488
19489 if ( md5( $license_or_user_key ) !== $stored_key ) {
19490 $this->shoot_ajax_failure( sprintf(
19491 '%s... %s',
19492 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
19493 $this->get_text_inline(
19494 'seems like the key you entered doesn\'t match our records.',
19495 'developer-or-license-not-found'
19496 )
19497 ) );
19498 }
19499 }
19500
19501 if ( $this->is_network_active() && fs_is_network_admin() ) {
19502 set_site_transient(
19503 "fs_{$this->get_id()}_data_debug_mode",
19504 $transient_value,
19505 WP_FS__TIME_24_HOURS_IN_SEC / 24
19506 );
19507 } else {
19508 set_transient(
19509 "fs_{$this->get_id()}_data_debug_mode",
19510 $transient_value,
19511 WP_FS__TIME_24_HOURS_IN_SEC / 24
19512 );
19513 }
19514
19515 if ( 'true' === $transient_value ) {
19516 $this->_admin_notices->add_sticky(
19517 $this->get_text_inline(
19518 '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.',
19519 'data_debug_mode_enabled'
19520 ),
19521 'data_debug_mode_enabled'
19522 );
19523 }
19524
19525 $this->shoot_ajax_success();
19526 }
19527
19528 /**
19529 * Check if a given license is active & valid (not expired).
19530 *
19531 * @author Vova Feldman (@svovaf)
19532 * @since 2.1.3
19533 *
19534 * @param FS_Plugin_License $license
19535 *
19536 * @return bool
19537 */
19538 private static function is_active_valid_license( $license ) {
19539 return (
19540 is_object( $license ) &&
19541 FS_Plugin_License::is_valid_id( $license->id ) &&
19542 $license->is_active() &&
19543 $license->is_valid()
19544 );
19545 }
19546
19547 /**
19548 * Checks if there's any site that is associated with an active & valid license.
19549 * This logic is used to determine if the admin can download the premium code base from a network level admin.
19550 *
19551 * @author Vova Feldman (@svovaf)
19552 * @since 2.1.3
19553 *
19554 * @return bool
19555 */
19556 function has_any_active_valid_license() {
19557 if ( ! fs_is_network_admin() ) {
19558 return $this->has_active_valid_license();
19559 }
19560
19561 $installs = $this->get_blog_install_map();
19562 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
19563
19564 foreach ( $installs as $blog_id => $install ) {
19565 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
19566 continue;
19567 }
19568
19569 foreach ( $all_plugin_licenses as $license ) {
19570 if ( $license->id == $install->license_id ) {
19571 if ( self::is_active_valid_license( $license ) ) {
19572 return true;
19573 }
19574 }
19575 }
19576 }
19577
19578 return false;
19579 }
19580
19581 /**
19582 * Check if site assigned with license with enabled features.
19583 *
19584 * @author Vova Feldman (@svovaf)
19585 * @since 1.0.6
19586 *
19587 * @return bool
19588 */
19589 function has_features_enabled_license() {
19590 return (
19591 is_object( $this->_license ) &&
19592 is_numeric( $this->_license->id ) &&
19593 $this->_license->is_features_enabled()
19594 );
19595 }
19596
19597 /**
19598 * Check if user is a trial or have feature enabled license.
19599 *
19600 * @author Vova Feldman (@svovaf)
19601 * @since 1.1.7
19602 *
19603 * @return bool
19604 */
19605 function can_use_premium_code() {
19606 return $this->is_trial() || $this->has_features_enabled_license();
19607 }
19608
19609 /**
19610 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
19611 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
19612 * the context user is authenticated.
19613 *
19614 * @author Leo Fajardo (@leorw)
19615 * @since 1.2.2
19616 *
19617 * @return bool
19618 */
19619 function is_user_admin() {
19620 /**
19621 * Require a super-admin when network activated, running from the network level OR if
19622 * running from the site level but not delegated the opt-in.
19623 *
19624 * @author Vova Feldman (@svovaf)
19625 * @since 2.0.0
19626 */
19627 if ( $this->_is_network_active &&
19628 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
19629 ) {
19630 return is_super_admin();
19631 }
19632
19633 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
19634 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
19635 }
19636
19637 /**
19638 * Sync site's plan.
19639 *
19640 * @author Vova Feldman (@svovaf)
19641 * @since 1.0.3
19642 *
19643 * @uses FS_Api
19644 *
19645 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
19646 * the admin.
19647 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
19648 * network-level "Account" page.
19649 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
19650 * `_sync_plugin_license` method in order to switch to the previous blog when sending
19651 * updates for a single site in case `execute_cron` has switched to a different blog.
19652 */
19653 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
19654 $this->_logger->entrance();
19655
19656 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
19657
19658 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
19659
19660 if ( $is_addon_sync ) {
19661 $this->_sync_addon_license( $plugin_id, $background );
19662 } else {
19663 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
19664 }
19665
19666 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
19667 }
19668
19669 /**
19670 * Sync plugin's add-on license.
19671 *
19672 * @author Vova Feldman (@svovaf)
19673 * @since 1.0.6
19674 * @uses FS_Api
19675 *
19676 * @param number $addon_id
19677 * @param bool $background
19678 */
19679 private function _sync_addon_license( $addon_id, $background ) {
19680 $this->_logger->entrance();
19681
19682 if ( $this->is_addon_activated( $addon_id ) ) {
19683 // If already installed, use add-on sync.
19684 $fs_addon = self::get_instance_by_id( $addon_id );
19685
19686 if (
19687 // Add-on is network activated and network integrated.
19688 $fs_addon->is_network_active() ||
19689 // Background sync cron.
19690 self::is_cron() ||
19691 // Add-on is not network activated or not network integrated.
19692 ! fs_is_network_admin()
19693 ) {
19694 $fs_addon->_sync_license( $background );
19695
19696 return;
19697 }
19698 }
19699
19700 // Validate add-on exists.
19701 $addon = $this->get_addon( $addon_id );
19702
19703 if ( ! is_object( $addon ) ) {
19704 return;
19705 }
19706
19707 // Add add-on into account add-ons.
19708 $account_addons = $this->get_account_addons();
19709 if ( ! is_array( $account_addons ) ) {
19710 $account_addons = array();
19711 }
19712 $account_addons[] = $addon->id;
19713 $account_addons = array_unique( $account_addons );
19714 $this->_store_account_addons( $account_addons );
19715
19716 // Load add-on licenses.
19717 $licenses = $this->_fetch_licenses( $addon->id );
19718
19719 // Sync add-on licenses.
19720 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
19721 $this->_update_licenses( $licenses, $addon->id );
19722
19723 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
19724 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
19725
19726 if ( ! isset( $plans_result->error ) ) {
19727 $plans = array();
19728 foreach ( $plans_result->plans as $plan ) {
19729 $plans[] = new FS_Plugin_Plan( $plan );
19730 }
19731
19732 $this->_admin_notices->add_sticky(
19733 sprintf(
19734 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
19735 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
19736 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
19737 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
19738 $addon->title
19739 ) . ' ' . $this->get_latest_download_link(
19740 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
19741 $addon_id
19742 ),
19743 'addon_plan_upgraded_' . $addon->slug,
19744 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
19745 );
19746 }
19747 }
19748 }
19749 }
19750
19751 /**
19752 * Sync site's plugin plan.
19753 *
19754 * @author Vova Feldman (@svovaf)
19755 * @since 1.0.6
19756 * @uses FS_Api
19757 *
19758 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
19759 * @param bool $send_installs_update Since 2.0.0
19760 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
19761 * syncing its license from the network-level "Account" page (e.g.: after
19762 * activating a license only for the single install).
19763 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
19764 * can be used here to switch to the previous blog in case `execute_cron`
19765 * has switched to a different blog.
19766 */
19767 private function _sync_plugin_license(
19768 $background = false,
19769 $send_installs_update = true,
19770 $is_context_single_site = false,
19771 $current_blog_id = null
19772 ) {
19773 $this->_logger->entrance();
19774
19775 $plan_change = 'none';
19776
19777 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
19778
19779 if ( ! $send_installs_update ) {
19780 $site = $this->_site;
19781 } else {
19782 /**
19783 * Sync site info.
19784 *
19785 * @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.
19786 */
19787 if ( $is_site_level_sync ) {
19788 /**
19789 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
19790 *
19791 * @author Leo Fajardo (@leorw)
19792 * @since 2.2.3
19793 */
19794 if ( is_numeric( $current_blog_id ) ) {
19795 $this->switch_to_blog( $current_blog_id );
19796 }
19797
19798 $result = $this->send_install_update( array(), true );
19799 $is_valid = $this->is_api_result_entity( $result );
19800 } else {
19801 $result = $this->send_installs_update( array(), true );
19802 $is_valid = $this->is_api_result_object( $result, 'installs' );
19803 }
19804
19805 if ( ! $is_valid ) {
19806 if ( $is_context_single_site ) {
19807 // Switch back to the main blog so that the following logic will have the right entities.
19808 $this->switch_to_blog( $this->_storage->network_install_blog_id );
19809 }
19810
19811 // Show API messages only if not background sync or if paying customer.
19812 if ( ! $background || $this->is_paying() ) {
19813 // Try to ping API to see if not blocked.
19814 if ( ! FS_Api::test() ) {
19815 /**
19816 * Failed to ping API - blocked!
19817 *
19818 * @author Vova Feldman (@svovaf)
19819 * @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.
19820 */
19821 $api = $this->get_api_site_scope();
19822
19823 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
19824 self::$_global_admin_notices->add(
19825 sprintf(
19826 $this->get_text_inline( 'Your server is blocking the access to Freemius\' API, which is crucial for %1$s synchronization. Please contact your host to whitelist %2$s', 'server-blocking-access' ),
19827 $this->get_plugin_name(),
19828 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
19829 'api.freemius.com',
19830 'wp.freemius.com'
19831 ) ) ) . '</b>'
19832 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
19833 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
19834 'error',
19835 $background,
19836 'api_blocked'
19837 );
19838 }
19839 } else {
19840 // Authentication params are broken.
19841 $this->_admin_notices->add(
19842 $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 ),
19843 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
19844 'error'
19845 );
19846 }
19847 }
19848
19849 // No reason to continue with license sync while there are API issues.
19850 return;
19851 }
19852
19853 if ( $is_site_level_sync ) {
19854 $site = new FS_Site( $result );
19855 } else {
19856 // Map site addresses to their blog IDs.
19857 $address_to_blog_map = $this->get_address_to_blog_map();
19858
19859 // Find the current context install.
19860 $site = null;
19861 foreach ( $result->installs as $install ) {
19862 if ( $install->id == $this->_site->id ) {
19863 $site = new FS_Site( $install );
19864 } else {
19865 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
19866 $blog_id = $address_to_blog_map[ $address ];
19867
19868 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
19869 }
19870 }
19871 }
19872
19873 // Sync plans.
19874 $this->_sync_plans();
19875 }
19876
19877 // Remove sticky API connectivity message.
19878 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
19879
19880 if ( ! $this->has_paid_plan() ) {
19881 $this->_site = $site;
19882 $this->_store_site(
19883 true,
19884 $is_site_level_sync ?
19885 null :
19886 $this->get_network_install_blog_id()
19887 );
19888 } else {
19889 $context_blog_id = 0;
19890
19891 if ( $is_context_single_site ) {
19892 $context_blog_id = get_current_blog_id();
19893
19894 // Switch back to the main blog in order to properly sync the license.
19895 $this->switch_to_blog( $this->_storage->network_install_blog_id );
19896 }
19897
19898 /**
19899 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
19900 * associated with that ID is not included in the user's licenses collection.
19901 */
19902 $this->_sync_licenses(
19903 $site->license_id,
19904 ( $is_context_single_site ?
19905 $context_blog_id :
19906 null
19907 )
19908 );
19909
19910 if ( $is_context_single_site ) {
19911 $this->switch_to_blog( $context_blog_id );
19912 }
19913
19914 // Check if plan / license changed.
19915 if ( $site->plan_id != $this->_site->plan_id ||
19916 // Check if trial started.
19917 $site->trial_plan_id != $this->_site->trial_plan_id ||
19918 $site->trial_ends != $this->_site->trial_ends ||
19919 // Check if license changed.
19920 $site->license_id != $this->_site->license_id
19921 ) {
19922 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
19923 // New trial started.
19924 $this->_site = $site;
19925 $plan_change = 'trial_started';
19926
19927 // For trial with subscription use-case.
19928 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
19929
19930 if ( is_object( $new_license ) && $new_license->is_valid() ) {
19931 $this->_site = $site;
19932 $this->_update_site_license( $new_license );
19933 $this->_store_licenses();
19934
19935 $this->_sync_site_subscription( $this->_license );
19936 }
19937 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
19938 // Was in trial, but now trial expired and no license ID.
19939 // New trial started.
19940 $this->_site = $site;
19941 $plan_change = 'trial_expired';
19942 } else {
19943 $is_free = $this->is_free_plan();
19944
19945 // Make sure license exist and not expired.
19946 $new_license = is_null( $site->license_id ) ?
19947 null :
19948 $this->_get_license_by_id( $site->license_id );
19949
19950 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
19951 // License cancelled.
19952 $this->_site = $site;
19953 $this->_update_site_license( $new_license );
19954 $this->_store_licenses();
19955
19956 $plan_change = 'cancelled';
19957 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
19958 // The license is expired, so ignore upgrade method.
19959 $this->_site = $site;
19960 } else {
19961 // License changed.
19962 $this->_site = $site;
19963
19964 /**
19965 * IMPORTANT:
19966 * 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.
19967 *
19968 * @author Vova Feldman (@svovaf)
19969 * @since 2.0.0
19970 */
19971 $this->_update_site_license( $new_license );
19972
19973 if ( ! $is_context_single_site &&
19974 fs_is_network_admin() &&
19975 $this->_is_network_active &&
19976 $new_license->quota > 1 &&
19977 get_blog_count() > 1
19978 ) {
19979 // See if license can activated on all sites.
19980 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
19981 if ( ! fs_request_get_bool( 'auto_install' ) ) {
19982 // Open the license activation dialog box on the account page.
19983 add_action( 'admin_footer', array(
19984 &$this,
19985 '_open_license_activation_dialog_box'
19986 ) );
19987 }
19988 }
19989 }
19990
19991 $this->_store_licenses();
19992
19993 $plan_change = $is_free ?
19994 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
19995 ( is_object( $new_license ) ?
19996 'changed' :
19997 'downgraded' );
19998 }
19999 }
20000
20001 // Store updated site info.
20002 $this->_store_site(
20003 true,
20004 $is_site_level_sync ?
20005 null :
20006 $this->get_network_install_blog_id()
20007 );
20008 } else {
20009 if ( ! is_object( $this->_license ) ) {
20010 $this->maybe_update_whitelabel_flag(
20011 FS_Plugin_License::is_valid_id( $site->license_id ) ?
20012 $this->get_license_by_id( $site->license_id ) :
20013 null
20014 );
20015 } else {
20016 $this->maybe_update_whitelabel_flag( $this->_license );
20017
20018 if ( $this->_license->is_expired() ) {
20019 if ( ! $this->has_features_enabled_license() ) {
20020 $this->_deactivate_license();
20021 $plan_change = 'downgraded';
20022 } else {
20023 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
20024
20025 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
20026 /**
20027 * Show the expired license notice every 14 days.
20028 *
20029 * @author Leo Fajardo (@leorw)
20030 * @since 2.3.1
20031 */
20032 $plan_change = 'expired';
20033 }
20034 }
20035 }
20036 }
20037
20038 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
20039 $this->_sync_site_subscription( $this->_license );
20040 }
20041 }
20042
20043 if ( $this->is_addon() || $this->has_addons() ) {
20044 /**
20045 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
20046 * an updated valid user licenses collection will be fetched from the server which is used to also
20047 * update the account add-ons (add-ons the user has licenses for).
20048 *
20049 * @author Leo Fajardo (@leorw)
20050 * @since 2.2.4
20051 */
20052 $this->purge_valid_user_licenses_cache();
20053 }
20054 }
20055
20056 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
20057
20058 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
20059 switch ( $plan_change ) {
20060 case 'none':
20061 if ( ! $background && is_admin() ) {
20062 $plan = $this->is_trial() ?
20063 $this->get_trial_plan() :
20064 $this->get_plan();
20065
20066 if ( $plan->is_free() ) {
20067 $this->_admin_notices->add(
20068 sprintf(
20069 $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' ),
20070 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
20071 ) . ' ' . sprintf(
20072 '<a href="%s">%s</a>',
20073 $this->contact_url(
20074 'bug',
20075 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' ),
20076 strtoupper( $plan->name )
20077 )
20078 ),
20079 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
20080 ),
20081 $hmm_text
20082 );
20083 }
20084 }
20085 break;
20086 case 'upgraded':
20087 case 'activated':
20088 $this->_admin_notices->add_sticky(
20089 ( 'activated' === $plan_change ) ?
20090 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ) :
20091 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) .
20092 $this->get_complete_upgrade_instructions(),
20093 'plan_upgraded',
20094 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20095 );
20096
20097 $this->_admin_notices->remove_sticky( array(
20098 'trial_started',
20099 'trial_promotion',
20100 'trial_expired',
20101 'activation_complete',
20102 'license_expired',
20103 ) );
20104 break;
20105 case 'changed':
20106 $this->_admin_notices->add_sticky(
20107 sprintf(
20108 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
20109 $this->get_plan_title()
20110 ),
20111 'plan_changed'
20112 );
20113
20114 $this->_admin_notices->remove_sticky( array(
20115 'trial_started',
20116 'trial_promotion',
20117 'trial_expired',
20118 'activation_complete',
20119 ) );
20120 break;
20121 case 'downgraded':
20122 $this->_admin_notices->add_sticky(
20123 ($this->has_free_plan() ?
20124 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 ) :
20125 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
20126 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) ) ),
20127 'license_expired',
20128 $hmm_text
20129 );
20130 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20131 break;
20132 case 'cancelled':
20133 $this->_admin_notices->add(
20134 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
20135 sprintf(
20136 '<a href="%s">%s</a>',
20137 $this->contact_url( 'bug' ),
20138 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
20139 ),
20140 $hmm_text,
20141 'error'
20142 );
20143 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20144 break;
20145 case 'expired':
20146 $this->_admin_notices->add_sticky(
20147 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 ),
20148 'license_expired',
20149 $hmm_text
20150 );
20151
20152 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
20153
20154 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20155 break;
20156 case 'trial_started':
20157 $this->_admin_notices->add_sticky(
20158 sprintf(
20159 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
20160 '<i>' . $this->get_plugin_name() . '</i>'
20161 ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ),
20162 'trial_started',
20163 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20164 );
20165
20166 $this->_admin_notices->remove_sticky( array(
20167 'trial_promotion',
20168 ) );
20169 break;
20170 case 'trial_expired':
20171 $this->_admin_notices->add_sticky(
20172 ($this->has_free_plan() ?
20173 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
20174 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
20175 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))),
20176 'trial_expired',
20177 $hmm_text
20178 );
20179 $this->_admin_notices->remove_sticky( array(
20180 'trial_started',
20181 'trial_promotion',
20182 'plan_upgraded',
20183 ) );
20184 break;
20185 }
20186 }
20187
20188 if ( 'none' !== $plan_change ) {
20189 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
20190 }
20191 }
20192
20193 /**
20194 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
20195 *
20196 * @author Vova Feldman (@svovaf)
20197 * @since 2.0.0
20198 */
20199 public function _open_license_activation_dialog_box() {
20200 $vars = array( 'license_id' => $this->_site->license_id );
20201 fs_require_once_template( 'js/open-license-activation.php', $vars );
20202 }
20203
20204 /**
20205 * @author Vova Feldman (@svovaf)
20206 * @since 1.0.5
20207 *
20208 * @param bool $background
20209 * @param FS_Plugin_License|null $premium_license
20210 */
20211 protected function _activate_license( $background = false, $premium_license = null ) {
20212 $this->_logger->entrance();
20213
20214 if ( is_null( $premium_license ) ) {
20215 $license_id = fs_request_get( 'license_id' );
20216
20217 if ( is_object( $this->_site ) &&
20218 FS_Plugin_License::is_valid_id( $license_id ) &&
20219 $license_id == $this->_site->license_id
20220 ) {
20221 // License is already activated.
20222 return;
20223 }
20224
20225 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
20226 $this->_get_license_by_id( $license_id ) :
20227 $this->_get_available_premium_license();
20228 }
20229
20230 if ( ! is_object( $premium_license ) ) {
20231 return;
20232 }
20233
20234 if ( ! is_object( $this->_site ) ) {
20235 // Not yet opted-in.
20236 $user = $this->get_current_or_network_user();
20237 if ( ! is_object( $user ) ) {
20238 $user = self::_get_user_by_id( $premium_license->user_id );
20239 }
20240
20241 if ( is_object( $user ) ) {
20242 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
20243 } else {
20244 $this->opt_in(
20245 false,
20246 false,
20247 false,
20248 $premium_license->secret_key
20249 );
20250
20251 return;
20252 }
20253 }
20254
20255
20256 /**
20257 * If the premium license is already associated with the install, just
20258 * update the license reference (activation is not required).
20259 *
20260 * @since 1.1.9
20261 */
20262 if ( $premium_license->id == $this->_site->license_id ) {
20263 // License is already activated.
20264 $this->_update_site_license( $premium_license );
20265 $this->_store_account();
20266
20267 return;
20268 }
20269
20270 if ( $this->_site->user_id != $premium_license->user_id ) {
20271 $api_request_params = array( 'license_key' => $premium_license->secret_key );
20272 } else {
20273 $api_request_params = array();
20274 }
20275
20276 $api = $this->get_api_site_scope();
20277 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
20278
20279 if ( ! $this->is_api_result_entity( $license ) ) {
20280 if ( ! $background ) {
20281 $this->_admin_notices->add( sprintf(
20282 '%s %s',
20283 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
20284 ( is_object( $license ) && isset( $license->error ) ?
20285 $license->error->message :
20286 sprintf( '%s<br><code>%s</code>',
20287 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
20288 var_export( $license, true )
20289 )
20290 )
20291 ),
20292 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
20293 'error'
20294 );
20295 }
20296
20297 return;
20298 }
20299
20300 $premium_license = new FS_Plugin_License( $license );
20301
20302 // Updated site plan.
20303 $site = $this->get_api_site_scope()->get( '/', true );
20304 if ( $this->is_api_result_entity( $site ) ) {
20305 $this->_site = new FS_Site( $site );
20306 }
20307 $this->_update_site_license( $premium_license );
20308
20309 $this->_store_account();
20310
20311 if ( $this->is_addon() || $this->has_addons() ) {
20312 /**
20313 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
20314 * an updated valid user licenses collection will be fetched from the server which is used to also
20315 * update the account add-ons (add-ons the user has licenses for).
20316 *
20317 * @author Leo Fajardo (@leorw)
20318 * @since 2.2.4
20319 */
20320 $this->purge_valid_user_licenses_cache();
20321 }
20322
20323 if ( ! $background ) {
20324 $this->_admin_notices->add_sticky(
20325 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
20326 $this->get_complete_upgrade_instructions(),
20327 'license_activated',
20328 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
20329 );
20330 }
20331
20332 $this->_admin_notices->remove_sticky( array(
20333 'trial_promotion',
20334 'license_expired',
20335 ) );
20336 }
20337
20338 /**
20339 * @author Vova Feldman (@svovaf)
20340 * @since 1.0.5
20341 *
20342 * @param bool $show_notice
20343 */
20344 protected function _deactivate_license( $show_notice = true ) {
20345 $this->_logger->entrance();
20346
20347 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
20348
20349 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
20350 $this->_admin_notices->add(
20351 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() ),
20352 $hmm_text
20353 );
20354
20355 return;
20356 }
20357
20358 $api = $this->get_api_site_scope();
20359 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
20360
20361 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
20362 }
20363
20364 /**
20365 * @author Leo Fajardo (@leorw)
20366 * @since 2.2.1
20367 *
20368 * @param FS_Plugin_License $license
20369 * @param bool|string $hmm_text
20370 * @param bool $show_notice
20371 */
20372 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
20373 if ( isset( $license->error ) ) {
20374 $this->_admin_notices->add(
20375 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
20376 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
20377 $hmm_text,
20378 'error'
20379 );
20380
20381 return;
20382 }
20383
20384 // Update license cache.
20385 if ( is_array( $this->_licenses ) ) {
20386 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
20387 if ( $license->id == $this->_licenses[ $i ]->id ) {
20388 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
20389 }
20390 }
20391 }
20392
20393 // Update site plan to default.
20394 $this->_sync_plans();
20395 $this->_site->plan_id = $this->_plans[0]->id;
20396 // Unlink license from site.
20397 $this->_update_site_license( null );
20398
20399 $this->_store_account();
20400
20401 if ( $show_notice ) {
20402 $this->_admin_notices->add(
20403 sprintf( $this->is_only_premium() ?
20404 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
20405 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
20406 $this->get_plan_title()
20407 ),
20408 $this->get_text_inline( 'O.K', 'ok' )
20409 );
20410 }
20411
20412 $this->_admin_notices->remove_sticky( array(
20413 'plan_upgraded',
20414 'license_activated',
20415 ) );
20416 }
20417
20418 /**
20419 * Site plan downgrade.
20420 *
20421 * @author Vova Feldman (@svovaf)
20422 * @since 1.0.4
20423 *
20424 * @return object
20425 *
20426 * @uses FS_Api
20427 */
20428 private function _downgrade_site() {
20429 $this->_logger->entrance();
20430
20431 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
20432
20433 $api = $this->get_api_site_scope();
20434 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
20435
20436 $plan_downgraded = false;
20437 $plan = false;
20438 if ( $this->is_api_result_entity( $site ) ) {
20439 $prev_plan_id = $this->_site->plan_id;
20440
20441 // Update new site plan id.
20442 $this->_site->plan_id = $site->plan_id;
20443
20444 $plan = $this->get_plan();
20445 $subscription = $this->_sync_site_subscription( $this->_license );
20446
20447 // Plan downgraded if plan was changed or subscription was cancelled.
20448 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
20449 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
20450 } else {
20451 // handle different error cases.
20452 $this->handle_license_deactivation_result(
20453 $site,
20454 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
20455 );
20456 }
20457
20458 if ( ! $plan_downgraded ) {
20459 return (object) array(
20460 'error' => (object) array(
20461 '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' )
20462 )
20463 );
20464 }
20465
20466 // Remove previous sticky message about upgrade (if exist).
20467 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
20468
20469 $this->_admin_notices->add(
20470 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
20471 $plan->title,
20472 human_time_diff( time(), strtotime( $this->_license->expiration ) )
20473 )
20474 );
20475
20476 // Store site updates.
20477 $this->_store_site();
20478
20479 if ( $deactivate_license &&
20480 ! FS_Plugin_License::is_valid_id( $site->license_id )
20481 ) {
20482 if ( $this->_site->is_localhost() ) {
20483 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
20484 } else {
20485 $this->_license->activated = max( 0, $this->_license->activated - 1 );
20486 }
20487
20488 // Handle successful license deactivation result.
20489 $this->handle_license_deactivation_result( $this->_license );
20490 }
20491
20492 return $site;
20493 }
20494
20495 /**
20496 * @author Vova Feldman (@svovaf)
20497 * @since 1.1.8.1
20498 *
20499 * @param bool|string $plan_name
20500 *
20501 * @return bool If trial was successfully started.
20502 */
20503 function start_trial( $plan_name = false ) {
20504 $this->_logger->entrance();
20505
20506 // Alias.
20507 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
20508
20509 if ( $this->is_trial() ) {
20510 // Already in trial mode.
20511 $this->_admin_notices->add(
20512 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
20513 $oops_text,
20514 'error'
20515 );
20516
20517 return false;
20518 }
20519
20520 if ( $this->_site->is_trial_utilized() ) {
20521 // Trial was already utilized.
20522 $this->_admin_notices->add(
20523 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
20524 $oops_text,
20525 'error'
20526 );
20527
20528 return false;
20529 }
20530
20531 if ( false !== $plan_name ) {
20532 $plan = $this->get_plan_by_name( $plan_name );
20533
20534 if ( false === $plan ) {
20535 // Plan doesn't exist.
20536 $this->_admin_notices->add(
20537 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
20538 $oops_text,
20539 'error'
20540 );
20541
20542 return false;
20543 }
20544
20545 if ( ! $plan->has_trial() ) {
20546 // Plan doesn't exist.
20547 $this->_admin_notices->add(
20548 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
20549 $oops_text,
20550 'error'
20551 );
20552
20553 return false;
20554 }
20555 } else {
20556 if ( ! $this->has_trial_plan() ) {
20557 // None of the plans have a trial.
20558 $this->_admin_notices->add(
20559 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
20560 $oops_text,
20561 'error'
20562 );
20563
20564 return false;
20565 }
20566
20567 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
20568
20569 $plan = $plans_with_trial[0];
20570 }
20571
20572 $api = $this->get_api_site_scope();
20573 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
20574
20575 if ( ! $this->is_api_result_entity( $plan ) ) {
20576 // Some API error while trying to start the trial.
20577 $this->_admin_notices->add(
20578 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type )
20579 . ' ' . var_export( $plan, true ),
20580 $oops_text,
20581 'error'
20582 );
20583
20584 return false;
20585 }
20586
20587 // Sync license.
20588 $this->_sync_license();
20589
20590 return $this->is_trial();
20591 }
20592
20593 /**
20594 * Cancel site trial.
20595 *
20596 * @author Vova Feldman (@svovaf)
20597 * @since 1.0.9
20598 *
20599 * @return object
20600 *
20601 * @uses FS_Api
20602 */
20603 private function _cancel_trial() {
20604 $this->_logger->entrance();
20605
20606 if ( ! $this->is_trial() ) {
20607 return (object) array(
20608 'error' => (object) array(
20609 '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' )
20610 )
20611 );
20612 }
20613
20614 $trial_plan = $this->get_trial_plan();
20615
20616 $api = $this->get_api_site_scope();
20617 $site = $api->call( 'trials.json', 'delete' );
20618
20619 $trial_cancelled = false;
20620
20621 if ( $this->is_api_result_entity( $site ) ) {
20622 $prev_trial_ends = $this->_site->trial_ends;
20623
20624 if ( $this->is_paid_trial() ) {
20625 $this->_license->expiration = $site->trial_ends;
20626 $this->_license->is_cancelled = true;
20627 $this->_update_site_license( $this->_license );
20628 $this->_store_licenses();
20629
20630 // Clear subscription reference.
20631 $this->_sync_site_subscription( null );
20632 }
20633
20634 // Update site info.
20635 $this->_site = new FS_Site( $site );
20636
20637 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
20638 } else {
20639 // @todo handle different error cases.
20640 }
20641
20642 if ( ! $trial_cancelled ) {
20643 return (object) array(
20644 'error' => (object) array(
20645 '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' )
20646 )
20647 );
20648 }
20649
20650 // Remove previous sticky messages about upgrade or trial (if exist).
20651 $this->_admin_notices->remove_sticky( array(
20652 'trial_started',
20653 'trial_promotion',
20654 'plan_upgraded',
20655 ) );
20656
20657 // Store site updates.
20658 $this->_store_site();
20659
20660 if ( ! $this->is_addon() ||
20661 ! $this->deactivate_premium_only_addon_without_license( true )
20662 ) {
20663 $this->_admin_notices->add(
20664 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
20665 );
20666 }
20667
20668 return $site;
20669 }
20670
20671 /**
20672 * @author Vova Feldman (@svovaf)
20673 * @since 1.0.6
20674 *
20675 * @param bool|number $plugin_id
20676 *
20677 * @return bool
20678 */
20679 private function _is_addon_id( $plugin_id ) {
20680 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
20681 }
20682
20683 /**
20684 * Check if user eligible to download premium version updates.
20685 *
20686 * @author Vova Feldman (@svovaf)
20687 * @since 1.0.6
20688 *
20689 * @return bool
20690 */
20691 private function _can_download_premium() {
20692 return $this->has_any_active_valid_license() ||
20693 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
20694 }
20695
20696 /**
20697 *
20698 * @author Vova Feldman (@svovaf)
20699 * @since 1.0.6
20700 *
20701 * @param bool|number $addon_id
20702 * @param string $type "json" or "zip"
20703 *
20704 * @return string
20705 */
20706 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
20707
20708 $is_addon = $this->_is_addon_id( $addon_id );
20709
20710 $is_premium = null;
20711 if ( ! $is_addon ) {
20712 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
20713 } else if ( $this->is_addon_activated( $addon_id ) ) {
20714 $fs_addon = self::get_instance_by_id( $addon_id );
20715 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
20716 }
20717
20718 // If add-on, then append add-on ID.
20719 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
20720 '/updates/latest.' . $type;
20721
20722 // If add-on and not yet activated, try to fetch based on server licensing.
20723 if ( is_bool( $is_premium ) ) {
20724 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
20725 }
20726
20727 if ( $this->has_secret_key() ) {
20728 $endpoint = add_query_arg( 'type', 'all', $endpoint );
20729 } else if ( $this->is_registered() && $this->_user->is_beta() ) {
20730 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
20731 }
20732
20733 return $endpoint;
20734 }
20735
20736 /**
20737 * @author Vova Feldman (@svovaf)
20738 * @since 1.0.4
20739 *
20740 * @param bool|number $addon_id
20741 * @param bool $flush Since 1.1.7.3
20742 * @param int $expiration Since 1.2.2.7
20743 * @param bool|string $newer_than Since 2.2.1
20744 * @param bool|string $fetch_readme Since 2.2.1
20745 *
20746 * @return object|false Plugin latest tag info.
20747 */
20748 function _fetch_latest_version(
20749 $addon_id = false,
20750 $flush = true,
20751 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
20752 $newer_than = false,
20753 $fetch_readme = true
20754 ) {
20755 $this->_logger->entrance();
20756
20757 $switch_to_blog_id = null;
20758
20759 /**
20760 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
20761 * @since 1.1.7.4 Also check updates for add-ons.
20762 */
20763 if ( ! $this->is_registered() &&
20764 ! $this->_is_addon_id( $addon_id )
20765 ) {
20766 if ( ! is_multisite() ) {
20767 return false;
20768 }
20769
20770 $installs_map = $this->get_blog_install_map();
20771
20772 foreach ( $installs_map as $blog_id => $install ) {
20773 /**
20774 * @var FS_Site $install
20775 */
20776 if ( $install->is_trial() ) {
20777 $switch_to_blog_id = $blog_id;
20778 break;
20779 }
20780
20781 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20782 $license = $this->get_license_by_id( $install->license_id );
20783
20784 if ( is_object( $license ) && $license->is_features_enabled() ) {
20785 $switch_to_blog_id = $blog_id;
20786 break;
20787 }
20788 }
20789 }
20790
20791 if ( is_null( $switch_to_blog_id ) ) {
20792 return false;
20793 }
20794 }
20795
20796 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
20797 get_current_blog_id() :
20798 0;
20799
20800 if ( is_numeric( $switch_to_blog_id ) ) {
20801 $this->switch_to_blog( $switch_to_blog_id );
20802 }
20803
20804 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
20805
20806 if ( ! empty( $newer_than ) ) {
20807 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
20808 }
20809
20810 if ( true === $fetch_readme ) {
20811 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
20812 }
20813
20814 $tag = $this->get_api_site_or_plugin_scope()->get(
20815 $latest_version_endpoint,
20816 $flush,
20817 $expiration
20818 );
20819
20820 if ( is_numeric( $switch_to_blog_id ) ) {
20821 $this->switch_to_blog( $current_blog_id );
20822 }
20823
20824 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
20825
20826 $this->_logger->departure( 'Latest version ' . $latest_version );
20827
20828 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
20829 }
20830
20831 #----------------------------------------------------------------------------------
20832 #region Download Plugin
20833 #----------------------------------------------------------------------------------
20834
20835 /**
20836 * Download latest plugin version, based on plan.
20837 *
20838 * Not like _download_latest(), this will redirect the page
20839 * to secure download url to prevent dual download (from FS to WP server,
20840 * and then from WP server to the client / browser).
20841 *
20842 * @author Vova Feldman (@svovaf)
20843 * @since 1.0.9
20844 *
20845 * @param bool|number $plugin_id
20846 *
20847 * @uses FS_Api
20848 * @uses wp_redirect()
20849 */
20850 private function download_latest_directly( $plugin_id = false ) {
20851 $this->_logger->entrance();
20852
20853 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
20854 }
20855
20856 /**
20857 * Get latest plugin FS API download URL.
20858 *
20859 * @author Vova Feldman (@svovaf)
20860 * @since 1.0.9
20861 *
20862 * @param bool|number $plugin_id
20863 *
20864 * @return string
20865 */
20866 private function get_latest_download_api_url( $plugin_id = false ) {
20867 $this->_logger->entrance();
20868
20869 return $this->get_api_site_scope()->get_signed_url(
20870 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
20871 );
20872 }
20873
20874 /**
20875 * Get payment invoice URL.
20876 *
20877 * @author Vova Feldman (@svovaf)
20878 * @since 1.2.0
20879 *
20880 * @param bool|number $payment_id
20881 *
20882 * @return string
20883 */
20884 function _get_invoice_api_url( $payment_id = false ) {
20885 $this->_logger->entrance();
20886
20887 $url = $this->get_api_user_scope()->get_signed_url(
20888 "/payments/{$payment_id}/invoice.pdf"
20889 );
20890
20891 if ( ! fs_starts_with( $url, 'https://' ) ) {
20892 // Always use HTTPS for invoices.
20893 $url = 'https' . substr( $url, 4 );
20894 }
20895
20896 return $url;
20897 }
20898
20899 /**
20900 * Get latest plugin download link.
20901 *
20902 * @author Vova Feldman (@svovaf)
20903 * @since 1.0.9
20904 *
20905 * @param string $label
20906 * @param bool|number $plugin_id
20907 *
20908 * @return string
20909 */
20910 private function get_latest_download_link( $label, $plugin_id = false ) {
20911 return sprintf(
20912 '<a target="_blank" href="%s">%s</a>',
20913 $this->_get_latest_download_local_url( $plugin_id ),
20914 $label
20915 );
20916 }
20917
20918 /**
20919 * Get latest plugin download local URL.
20920 *
20921 * @author Vova Feldman (@svovaf)
20922 * @since 1.0.9
20923 *
20924 * @param bool|number $plugin_id
20925 *
20926 * @return string
20927 */
20928 function _get_latest_download_local_url( $plugin_id = false ) {
20929 // Add timestamp to protect from caching.
20930 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
20931
20932 if ( ! empty( $plugin_id ) ) {
20933 $params['plugin_id'] = $plugin_id;
20934 } else if ( $this->is_addon() ) {
20935 $params['plugin_id'] = $this->get_id();
20936 }
20937
20938 $fs = $this->is_addon() ?
20939 $this->get_parent_instance() :
20940 $this;
20941
20942 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
20943 }
20944
20945 #endregion Download Plugin ------------------------------------------------------------------
20946
20947 /**
20948 * @author Vova Feldman (@svovaf)
20949 * @since 1.0.4
20950 *
20951 * @uses FS_Api
20952 *
20953 * @param bool $background Hints the method if it's a background updates check. If false, it means that
20954 * was initiated by the admin.
20955 * @param bool|number $plugin_id
20956 * @param bool $flush Since 1.1.7.3
20957 * @param int $expiration Since 1.2.2.7
20958 * @param bool|string $newer_than Since 2.2.1
20959 */
20960 private function check_updates(
20961 $background = false,
20962 $plugin_id = false,
20963 $flush = true,
20964 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
20965 $newer_than = false
20966 ) {
20967 $this->_logger->entrance();
20968
20969 // Check if there's a newer version for download.
20970 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
20971
20972 $update = null;
20973 if ( is_object( $new_version ) ) {
20974 $update = new FS_Plugin_Tag( $new_version );
20975
20976 if ( ! $background ) {
20977 $this->_admin_notices->add(
20978 sprintf(
20979 /* translators: %s: Numeric version number (e.g. '2.1.9' */
20980 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
20981 $update->version,
20982 sprintf(
20983 '<a href="%s" target="_blank">%s</a>',
20984 $this->get_account_url( 'download_latest' ),
20985 sprintf(
20986 /* translators: %s: plan name (e.g. latest "Professional" version) */
20987 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
20988 $this->get_plan_title()
20989 )
20990 )
20991 ),
20992 $this->get_text_inline( 'New', 'new' ) . '!'
20993 );
20994 }
20995 } else if ( false === $new_version && ! $background ) {
20996 $this->_admin_notices->add(
20997 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
20998 $this->get_text_inline( 'You are all good!', 'you-are-good' )
20999 );
21000 }
21001
21002 $this->_store_update( $update, true, $plugin_id );
21003 }
21004
21005 /**
21006 * @author Vova Feldman (@svovaf)
21007 * @since 1.0.4
21008 *
21009 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
21010 *
21011 * @return FS_Plugin[]
21012 *
21013 * @uses FS_Api
21014 */
21015 private function sync_addons( $flush = false ) {
21016 $this->_logger->entrance();
21017
21018 $api = $this->get_api_site_or_plugin_scope();
21019
21020 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
21021
21022 /**
21023 * @since 1.2.1
21024 *
21025 * If there's a cached version of the add-ons and not asking
21026 * for a flush, just use the currently stored add-ons.
21027 */
21028 if ( ! $flush && $api->is_cached( $path ) ) {
21029 $addons = self::get_all_addons();
21030
21031 return isset( $addons[ $this->_plugin->id ] ) ?
21032 $addons[ $this->_plugin->id ] :
21033 array();
21034 }
21035
21036 $result = $api->get( $path, $flush );
21037
21038 $addons = array();
21039 if ( $this->is_api_result_object( $result, 'plugins' ) &&
21040 is_array( $result->plugins )
21041 ) {
21042 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
21043 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
21044 }
21045
21046 $this->_store_addons( $addons, true );
21047 }
21048
21049 return $addons;
21050 }
21051
21052 /**
21053 * Handle user email update.
21054 *
21055 * @author Vova Feldman (@svovaf)
21056 * @since 1.0.3
21057 * @uses FS_Api
21058 *
21059 * @param string $new_email
21060 *
21061 * @return object
21062 */
21063 private function update_email( $new_email ) {
21064 $this->_logger->entrance();
21065
21066
21067 $api = $this->get_api_user_scope();
21068 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
21069 'email' => $new_email,
21070 'after_email_confirm_url' => $this->_get_admin_page_url(
21071 'account',
21072 array( 'fs_action' => 'sync_user' )
21073 ),
21074 ) );
21075
21076 if ( ! isset( $user->error ) ) {
21077 $this->_user->email = $user->email;
21078 $this->_user->is_verified = $user->is_verified;
21079 $this->_store_user();
21080 } else {
21081 // handle different error cases.
21082
21083 }
21084
21085 return $user;
21086 }
21087
21088 #----------------------------------------------------------------------------------
21089 #region API Error Handling
21090 #----------------------------------------------------------------------------------
21091
21092 /**
21093 * @author Vova Feldman (@svovaf)
21094 * @since 1.1.1
21095 *
21096 * @param mixed $result
21097 *
21098 * @return bool Is API result contains an error.
21099 */
21100 private function is_api_error( $result ) {
21101 return FS_Api::is_api_error( $result );
21102 }
21103
21104 /**
21105 * Checks if given API result is a non-empty and not an error object.
21106 *
21107 * @author Vova Feldman (@svovaf)
21108 * @since 1.2.1.5
21109 *
21110 * @param mixed $result
21111 * @param string|null $required_property Optional property we want to verify that is set.
21112 *
21113 * @return bool
21114 */
21115 function is_api_result_object( $result, $required_property = null ) {
21116 return FS_Api::is_api_result_object( $result, $required_property );
21117 }
21118
21119 /**
21120 * Checks if given API result is a non-empty entity object with non-empty ID.
21121 *
21122 * @author Vova Feldman (@svovaf)
21123 * @since 1.2.1.5
21124 *
21125 * @param mixed $result
21126 *
21127 * @return bool
21128 */
21129 private function is_api_result_entity( $result ) {
21130 return FS_Api::is_api_result_entity( $result );
21131 }
21132
21133 #endregion
21134
21135 /**
21136 * Make sure a given argument is an array of a specific type.
21137 *
21138 * @author Vova Feldman (@svovaf)
21139 * @since 1.2.1.5
21140 *
21141 * @param mixed $array
21142 * @param string $class
21143 *
21144 * @return bool
21145 */
21146 private function is_array_instanceof( $array, $class ) {
21147 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
21148 }
21149
21150 /**
21151 * Start install ownership change.
21152 *
21153 * @author Vova Feldman (@svovaf)
21154 * @since 1.1.1
21155 * @uses FS_Api
21156 *
21157 * @param string $candidate_email
21158 *
21159 * @return bool Is ownership change successfully initiated.
21160 */
21161 private function init_change_owner( $candidate_email ) {
21162 $this->_logger->entrance();
21163
21164 $api = $this->get_api_site_scope();
21165 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
21166 'email' => $candidate_email,
21167 'after_confirm_url' => $this->_get_admin_page_url(
21168 'account',
21169 array( 'fs_action' => 'change_owner' )
21170 ),
21171 ) );
21172
21173 return ! $this->is_api_error( $result );
21174 }
21175
21176 /**
21177 * Handle install ownership change.
21178 *
21179 * @author Vova Feldman (@svovaf)
21180 * @since 1.1.1
21181 * @uses FS_Api
21182 *
21183 * @return bool Was ownership change successfully complete.
21184 */
21185 private function complete_change_owner() {
21186 $this->_logger->entrance();
21187
21188 $site_result = $this->get_api_site_scope( true )->get();
21189 $site = new FS_Site( $site_result );
21190 $this->_site = $site;
21191
21192 $user = new FS_User();
21193 $user->id = fs_request_get( 'user_id' );
21194
21195 // Validate install's user and given user.
21196 if ( $user->id != $this->_site->user_id ) {
21197 return false;
21198 }
21199
21200 $user->public_key = fs_request_get( 'user_public_key' );
21201 $user->secret_key = fs_request_get( 'user_secret_key' );
21202
21203 // Fetch new user information.
21204 $this->_user = $user;
21205 $user_result = $this->get_api_user_scope( true )->get();
21206 $user = new FS_User( $user_result );
21207 $this->_user = $user;
21208
21209 $this->_set_account( $user, $site );
21210
21211 return true;
21212 }
21213
21214 /**
21215 * Completes ownership change by license.
21216 *
21217 * @author Leo Fajardo (@leorw)
21218 * @since 2.3.2
21219 *
21220 * @param number $user_id
21221 * @param array[string]number $install_ids_by_slug_map
21222 *
21223 */
21224 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
21225 $this->_logger->entrance();
21226
21227 $this->sync_user_by_current_install( $user_id );
21228
21229 $result = $this->get_api_user_scope( true )->get(
21230 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
21231 );
21232
21233 if ( $this->is_api_result_object( $result, 'installs' ) ) {
21234 $sites = self::get_all_sites( $this->get_module_type() );
21235 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
21236
21237 foreach ( $result->installs as $install ) {
21238 $site = new FS_Site( $install );
21239
21240 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
21241 }
21242
21243 $this->set_account_option( 'sites', $sites, true );
21244 }
21245 }
21246
21247 /**
21248 * Handle user name update.
21249 *
21250 * @author Vova Feldman (@svovaf)
21251 * @since 1.0.9
21252 * @uses FS_Api
21253 *
21254 * @return object
21255 */
21256 private function update_user_name() {
21257 $this->_logger->entrance();
21258 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
21259
21260 $api = $this->get_api_user_scope();
21261 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
21262 'name' => $name,
21263 ) );
21264
21265 if ( ! isset( $user->error ) ) {
21266 $this->_user->first = $user->first;
21267 $this->_user->last = $user->last;
21268 $this->_store_user();
21269 } else {
21270 // handle different error cases.
21271
21272 }
21273
21274 return $user;
21275 }
21276
21277 /**
21278 * Verify user email.
21279 *
21280 * @author Vova Feldman (@svovaf)
21281 * @since 1.0.3
21282 * @uses FS_Api
21283 */
21284 private function verify_email() {
21285 $this->_handle_account_user_sync();
21286
21287 if ( $this->_user->is_verified() ) {
21288 return;
21289 }
21290
21291 $api = $this->get_api_site_scope();
21292 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
21293 'after_email_confirm_url' => $this->_get_admin_page_url(
21294 'account',
21295 array( 'fs_action' => 'sync_user' )
21296 )
21297 ) );
21298
21299 if ( ! isset( $result->error ) ) {
21300 $this->_admin_notices->add( sprintf(
21301 $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' ),
21302 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
21303 ) );
21304 } else {
21305 // handle different error cases.
21306
21307 }
21308 }
21309
21310 /**
21311 * @author Vova Feldman (@svovaf)
21312 * @since 1.1.2
21313 *
21314 * @param array $params
21315 * @param bool|null $network
21316 *
21317 * @return string
21318 */
21319 function get_activation_url( $params = array(), $network = null ) {
21320 if ( $this->is_addon() && $this->has_free_plan() ) {
21321 /**
21322 * @author Vova Feldman (@svovaf)
21323 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
21324 */
21325 return $this->get_parent_instance()->get_activation_url( $params );
21326 }
21327
21328 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
21329 }
21330
21331 /**
21332 * @author Vova Feldman (@svovaf)
21333 * @since 1.2.1.5
21334 *
21335 * @param array $params
21336 *
21337 * @return string
21338 */
21339 function get_reconnect_url( $params = array() ) {
21340 $params['fs_action'] = 'reset_anonymous_mode';
21341 $params['fs_unique_affix'] = $this->get_unique_affix();
21342
21343 return $this->get_activation_url( $params );
21344 }
21345
21346 /**
21347 * Get the URL of the page that should be loaded after the user connect
21348 * or skip in the opt-in screen.
21349 *
21350 * @author Vova Feldman (@svovaf)
21351 * @since 1.1.3
21352 *
21353 * @param string $filter Filter name.
21354 * @param array $params Since 1.2.2.7
21355 * @param bool|null $network
21356 *
21357 * @return string
21358 */
21359 function get_after_activation_url( $filter, $params = array(), $network = null ) {
21360 if ( $this->show_opt_in_on_themes_page() &&
21361 ( fs_request_has( 'pending_activation' ) ||
21362 // For cases when the first time path is set, even though it's a WP.org theme.
21363 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
21364 ) {
21365 $first_time_path = '';
21366 } else {
21367 $first_time_path = $this->_menu->get_first_time_path(
21368 fs_is_network_admin() && $this->_is_network_active
21369 );
21370 }
21371
21372 if ( $this->_is_network_active &&
21373 fs_is_network_admin() &&
21374 ! $this->_menu->has_network_menu() &&
21375 $this->is_network_registered()
21376 ) {
21377 $target_url = $this->get_account_url();
21378 } else {
21379 // Default plugin's page.
21380 $target_url = $this->_get_admin_page_url( '', array(), $network );
21381 }
21382
21383 return add_query_arg( $params, $this->apply_filters(
21384 $filter,
21385 empty( $first_time_path ) ?
21386 $target_url :
21387 $first_time_path
21388 ) );
21389 }
21390
21391 /**
21392 * Handle account page updates / edits / actions.
21393 *
21394 * @author Vova Feldman (@svovaf)
21395 * @since 1.0.2
21396 *
21397 */
21398 private function _handle_account_edits() {
21399 if ( ! $this->is_user_admin() ) {
21400 return;
21401 }
21402
21403 $action = fs_get_action();
21404
21405 if ( empty( $action ) ) {
21406 return;
21407 }
21408
21409 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21410 $install_id = fs_request_get( 'install_id', '' );
21411
21412 // Alias.
21413 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21414
21415 $is_network_action = $this->is_network_level_action();
21416 $blog_id = $this->is_network_level_site_specific_action();
21417 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
21418
21419 if ( is_numeric( $blog_id ) ) {
21420 $this->switch_to_blog( $blog_id );
21421 } else {
21422 $blog_id = '';
21423 }
21424
21425 switch ( $action ) {
21426 case 'opt_in':
21427 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
21428
21429 if ( $is_parent_plugin_action ) {
21430 if ( $is_network_action && ! empty( $blog_id ) ) {
21431 if ( ! $this->is_registered() ) {
21432 $this->install_with_user(
21433 $this->get_network_user(),
21434 false,
21435 false,
21436 false,
21437 false
21438 );
21439
21440 $this->_admin_notices->add(
21441 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
21442 $this->get_text_inline( 'Awesome', 'awesome' )
21443 );
21444 }
21445 }
21446 }
21447 break;
21448
21449 case 'toggle_tracking':
21450 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
21451
21452 if ( $is_parent_plugin_action ) {
21453 if ( $is_network_action && ! empty( $blog_id ) ) {
21454 if ( $this->is_registered() ) {
21455 if ( $this->is_tracking_prohibited() ) {
21456 if ( $this->allow_site_tracking() ) {
21457 $this->_admin_notices->add(
21458 sprintf( $this->get_text_inline( 'We appreciate your help in making the %s better by letting us track some usage data.', 'opt-out-message-appreciation' ), $this->_module_type ),
21459 $this->get_text_inline( 'Thank you!', 'thank-you' )
21460 );
21461 }
21462 } else {
21463 if ( $this->stop_site_tracking() ) {
21464 $this->_admin_notices->add(
21465 sprintf(
21466 $this->get_text_inline( 'We will no longer be sending any usage data of %s on %s to %s.', 'opted-out-successfully' ),
21467 $this->get_plugin_title(),
21468 fs_strip_url_protocol( get_site_url( $blog_id ) ),
21469 sprintf(
21470 '<a href="%s" target="_blank">%s</a>',
21471 'https://freemius.com',
21472 'freemius.com'
21473 )
21474 )
21475 );
21476 }
21477 }
21478 }
21479 }
21480 }
21481
21482 break;
21483
21484 case 'delete_account':
21485 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
21486
21487 $is_network_deletion = $is_network_action && empty( $blog_id );
21488
21489 if ( $is_parent_plugin_action ) {
21490 // Delete add-on installs if have any.
21491 $installed_addons = $this->get_installed_addons();
21492 foreach ( $installed_addons as $fs_addon ) {
21493 if ( $is_network_deletion ) {
21494 $fs_addon->delete_network_account_event();
21495 } else {
21496 $fs_addon->delete_account_event();
21497 }
21498 }
21499
21500 if ( $is_network_deletion ) {
21501 $this->delete_network_account_event();
21502 } else {
21503 $this->delete_account_event();
21504 }
21505
21506 // Clear user and site.
21507 $this->_site = null;
21508 $this->_user = null;
21509
21510 $this->maybe_set_slug_and_network_menu_exists_flag();
21511
21512 fs_redirect( $this->get_activation_url() );
21513 } else {
21514 if ( $this->is_addon_activated( $plugin_id ) ) {
21515 $fs_addon = self::get_instance_by_id( $plugin_id );
21516
21517 if ( $is_network_deletion ) {
21518 $fs_addon->delete_network_account_event();
21519 } else {
21520 $fs_addon->delete_account_event();
21521 }
21522
21523 fs_redirect( $this->_get_admin_page_url( 'account' ) );
21524 }
21525 }
21526
21527 return;
21528
21529 case 'downgrade_account':
21530 if ( is_numeric( $blog_id ) ) {
21531 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
21532 } else {
21533 check_admin_referer( $action );
21534 }
21535
21536 $switch_to_network_install_blog_after_cancellation = (
21537 is_numeric( $blog_id ) &&
21538 $plugin_id == $this->get_id() &&
21539 ! $this->is_trial()
21540 );
21541
21542 $result = $this->cancel_subscription_or_trial( $plugin_id );
21543 if ( $this->is_api_error( $result ) ) {
21544 $this->_admin_notices->add(
21545 $result->error->message,
21546 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
21547 'error'
21548 );
21549 }
21550
21551 if ( $switch_to_network_install_blog_after_cancellation ) {
21552 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21553 }
21554
21555 return;
21556
21557 case 'activate_license':
21558 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
21559
21560 $fs = $this;
21561 if ( $plugin_id != $this->get_id() ) {
21562 $fs = $this->is_addon_activated( $plugin_id ) ?
21563 self::get_instance_by_id( $plugin_id ) :
21564 null;
21565 }
21566
21567 if ( is_object( $fs ) ) {
21568 $fs->_activate_license();
21569 }
21570
21571 return;
21572
21573 case 'deactivate_license':
21574 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
21575
21576 if ( $plugin_id == $this->get_id() ) {
21577 $this->_deactivate_license();
21578
21579 if ( $this->is_only_premium() ) {
21580 // Clear user and site.
21581 $this->_site = null;
21582 $this->_user = null;
21583
21584 if ( ! $is_network_action ) {
21585 fs_redirect( $this->get_activation_url() );
21586 } else if ( is_numeric( $blog_id ) ) {
21587 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21588 }
21589 }
21590 } else {
21591 if ( $this->is_addon_activated( $plugin_id ) ) {
21592 $fs_addon = self::get_instance_by_id( $plugin_id );
21593 $fs_addon->_deactivate_license();
21594 }
21595 }
21596
21597 return;
21598
21599 case 'check_updates':
21600 check_admin_referer( $action );
21601 $this->check_updates();
21602
21603 return;
21604
21605 case 'change_owner':
21606 $state = fs_request_get( 'state', 'init' );
21607 switch ( $state ) {
21608 case 'init':
21609 $candidate_email = fs_request_get( 'candidate_email', '' );
21610
21611 if ( $this->init_change_owner( $candidate_email ) ) {
21612 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Please check your mailbox, you should receive an email via %s to confirm the ownership change. From security reasons, you must confirm the change within the next 15 min. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '<b>' . $this->_user->email . '</b>' ) );
21613 }
21614 break;
21615 case 'owner_confirmed':
21616 $candidate_email = fs_request_get( 'candidate_email', '' );
21617
21618 $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>' ) );
21619 break;
21620 case 'candidate_confirmed':
21621 if ( $this->complete_change_owner() ) {
21622 $this->_admin_notices->add_sticky(
21623 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
21624 'ownership_changed',
21625 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
21626 );
21627 } else {
21628 // @todo Handle failed ownership change message.
21629 }
21630 break;
21631 }
21632
21633 return;
21634
21635 case 'update_email':
21636 check_admin_referer( 'update_email' );
21637
21638 $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' );
21639 $result = $this->update_email( $new_email );
21640
21641 if ( isset( $result->error ) ) {
21642 switch ( $result->error->code ) {
21643 case 'user_exist':
21644 $this->_admin_notices->add(
21645 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
21646 sprintf( $this->get_text_inline( 'If you would like to give up the ownership of the %s\'s account to %s click the Change Ownership button.', 'user-exist-message_ownership' ), $this->_module_type, '<b>' . $new_email . '</b>' ) .
21647 sprintf(
21648 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
21649 $this->get_account_url( 'change_owner', array(
21650 'state' => 'init',
21651 'candidate_email' => $new_email
21652 ) ),
21653 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
21654 ),
21655 $oops_text,
21656 'error'
21657 );
21658 break;
21659 }
21660 } else {
21661 $this->_admin_notices->add( $this->get_text_inline( 'Your email was successfully updated. You should receive an email with confirmation instructions in few moments.', 'email-updated-message' ) );
21662 }
21663
21664 return;
21665
21666 case 'update_user_name':
21667 check_admin_referer( 'update_user_name' );
21668
21669 $result = $this->update_user_name();
21670
21671 if ( isset( $result->error ) ) {
21672 $this->_admin_notices->add(
21673 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
21674 $oops_text,
21675 'error'
21676 );
21677 } else {
21678 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
21679 }
21680
21681 return;
21682
21683 #region Actions that might be called from external links (e.g. email)
21684
21685 case 'cancel_trial':
21686 $result = $this->cancel_subscription_or_trial( $plugin_id );
21687 if ( $this->is_api_error( $result ) ) {
21688 $this->_admin_notices->add(
21689 $result->error->message,
21690 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
21691 'error'
21692 );
21693 }
21694
21695 return;
21696
21697 case 'verify_email':
21698 $this->verify_email();
21699
21700 return;
21701
21702 case 'sync_user':
21703 $this->_handle_account_user_sync();
21704
21705 return;
21706
21707 case $this->get_unique_affix() . '_sync_license':
21708 $this->_sync_license();
21709
21710 return;
21711
21712 case 'download_latest':
21713 $this->download_latest_directly( $plugin_id );
21714
21715 return;
21716
21717 #endregion
21718 }
21719
21720 if ( WP_FS__IS_POST_REQUEST ) {
21721 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
21722 foreach ( $properties as $p ) {
21723 if ( 'update_' . $p === $action ) {
21724 check_admin_referer( $action );
21725
21726 $this->_logger->log( $action );
21727
21728 $site_property = substr( $p, strlen( 'site_' ) );
21729 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
21730 $this->get_site()->{$site_property} = $site_property_value;
21731
21732 // Store account after modification.
21733 $this->_store_site();
21734
21735 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
21736
21737 $this->_admin_notices->add( sprintf(
21738 /* translators: %s: User's account property (e.g. email address, name) */
21739 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
21740 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
21741 ) );
21742
21743 return;
21744 }
21745 }
21746 }
21747 }
21748
21749 /**
21750 * Account page resources load.
21751 *
21752 * @author Vova Feldman (@svovaf)
21753 * @since 1.0.6
21754 */
21755 function _account_page_load() {
21756 $this->_logger->entrance();
21757
21758 $this->_logger->info( var_export( $_REQUEST, true ) );
21759
21760 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
21761
21762 if ( $this->has_addons() ) {
21763 wp_enqueue_script( 'plugin-install' );
21764 add_thickbox();
21765
21766 function fs_addons_body_class( $classes ) {
21767 $classes .= ' plugins-php';
21768
21769 return $classes;
21770 }
21771
21772 add_filter( 'admin_body_class', 'fs_addons_body_class' );
21773 }
21774
21775 if ( $this->has_paid_plan() &&
21776 ! $this->has_any_license() &&
21777 ! $this->is_sync_executed() &&
21778 $this->is_tracking_allowed()
21779 ) {
21780 /**
21781 * If no licenses found and no sync job was executed during the last 24 hours,
21782 * just execute the sync job right away (blocking execution).
21783 *
21784 * @since 1.1.7.3
21785 */
21786 $this->run_manual_sync();
21787 }
21788
21789 $this->_handle_account_edits();
21790
21791 $this->do_action( 'account_page_load_before_departure' );
21792 }
21793
21794 /**
21795 * Renders the "Affiliation" page.
21796 *
21797 * @author Leo Fajardo (@leorw)
21798 * @since 1.2.3
21799 */
21800 function _affiliation_page_render() {
21801 $this->_logger->entrance();
21802
21803 $this->fetch_affiliate_and_terms();
21804
21805 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
21806
21807 $vars = array( 'id' => $this->_module_id );
21808 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
21809 }
21810
21811
21812 /**
21813 * Render account page.
21814 *
21815 * @author Vova Feldman (@svovaf)
21816 * @since 1.0.0
21817 */
21818 function _account_page_render() {
21819 $this->_logger->entrance();
21820
21821 $template = 'account.php';
21822 $vars = array( 'id' => $this->_module_id );
21823
21824 /**
21825 * Added filter to the template to allow developers wrapping the template
21826 * in custom HTML (e.g. within a wizard/tabs).
21827 *
21828 * @author Vova Feldman (@svovaf)
21829 * @since 1.2.1.6
21830 */
21831 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
21832 }
21833
21834 /**
21835 * Render account connect page.
21836 *
21837 * @author Vova Feldman (@svovaf)
21838 * @since 1.0.7
21839 */
21840 function _connect_page_render() {
21841 $this->_logger->entrance();
21842
21843 $vars = array( 'id' => $this->_module_id );
21844
21845 /**
21846 * Added filter to the template to allow developers wrapping the template
21847 * in custom HTML (e.g. within a wizard/tabs).
21848 *
21849 * @author Vova Feldman (@svovaf)
21850 * @since 1.2.1.6
21851 */
21852 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
21853 }
21854
21855 /**
21856 * Load required resources before add-ons page render.
21857 *
21858 * @author Vova Feldman (@svovaf)
21859 * @since 1.0.6
21860 */
21861 function _addons_page_load() {
21862 $this->_logger->entrance();
21863
21864 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
21865
21866 wp_enqueue_script( 'plugin-install' );
21867 add_thickbox();
21868
21869 function fs_addons_body_class( $classes ) {
21870 $classes .= ' plugins-php';
21871
21872 return $classes;
21873 }
21874
21875 add_filter( 'admin_body_class', 'fs_addons_body_class' );
21876
21877 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
21878 $this->_admin_notices->add(
21879 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>' ),
21880 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
21881 'update-nag'
21882 );
21883 }
21884 }
21885
21886 /**
21887 * Render add-ons page.
21888 *
21889 * @author Vova Feldman (@svovaf)
21890 * @since 1.0.6
21891 */
21892 function _addons_page_render() {
21893 $this->_logger->entrance();
21894
21895 $vars = array( 'id' => $this->_module_id );
21896
21897 /**
21898 * Added filter to the template to allow developers wrapping the template
21899 * in custom HTML (e.g. within a wizard/tabs).
21900 *
21901 * @author Vova Feldman (@svovaf)
21902 * @since 1.2.1.6
21903 */
21904 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
21905 }
21906
21907 /* Pricing & Upgrade
21908 ------------------------------------------------------------------------------------------------------------------*/
21909 /**
21910 * Render pricing page.
21911 *
21912 * @author Vova Feldman (@svovaf)
21913 * @since 1.0.0
21914 */
21915 function _pricing_page_render() {
21916 $this->_logger->entrance();
21917
21918 $vars = array( 'id' => $this->_module_id );
21919
21920 if ( 'true' === fs_request_get( 'checkout', false ) ) {
21921 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
21922 } else {
21923 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
21924 }
21925 }
21926
21927 #----------------------------------------------------------------------------------
21928 #region Contact Us
21929 #----------------------------------------------------------------------------------
21930
21931 /**
21932 * Render contact-us page.
21933 *
21934 * @author Vova Feldman (@svovaf)
21935 * @since 1.0.3
21936 */
21937 function _contact_page_render() {
21938 $this->_logger->entrance();
21939
21940 $vars = array( 'id' => $this->_module_id );
21941
21942 /**
21943 * Added filter to the template to allow developers wrapping the template
21944 * in custom HTML (e.g. within a wizard/tabs).
21945 *
21946 * @author Vova Feldman (@svovaf)
21947 * @since 2.1.3
21948 */
21949 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
21950 }
21951
21952 #endregion ------------------------------------------------------------------------
21953
21954 /**
21955 * Hide all admin notices to prevent distractions.
21956 *
21957 * @author Vova Feldman (@svovaf)
21958 * @since 1.0.3
21959 *
21960 * @uses remove_all_actions()
21961 */
21962 private static function _hide_admin_notices() {
21963 remove_all_actions( 'admin_notices' );
21964 remove_all_actions( 'network_admin_notices' );
21965 remove_all_actions( 'all_admin_notices' );
21966 remove_all_actions( 'user_admin_notices' );
21967 }
21968
21969 static function _clean_admin_content_section_hook() {
21970 self::_hide_admin_notices();
21971
21972 // Hide footer.
21973 echo '<style>#wpfooter { display: none !important; }</style>';
21974 }
21975
21976 /**
21977 * Attach to admin_head hook to hide all admin notices.
21978 *
21979 * @author Vova Feldman (@svovaf)
21980 * @since 1.0.3
21981 */
21982 static function _clean_admin_content_section() {
21983 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
21984 }
21985
21986 /* CSS & JavaScript
21987 ------------------------------------------------------------------------------------------------------------------*/
21988 /* function _enqueue_script($handle, $src) {
21989 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
21990
21991 $this->_logger->entrance( 'script = ' . $url );
21992
21993 wp_enqueue_script( $handle, $url );
21994 }*/
21995
21996 /* SDK
21997 ------------------------------------------------------------------------------------------------------------------*/
21998 private $_user_api;
21999
22000 /**
22001 *
22002 * @author Vova Feldman (@svovaf)
22003 * @since 1.0.2
22004 *
22005 * @param bool $flush
22006 *
22007 * @return FS_Api
22008 */
22009 private function get_api_user_scope( $flush = false ) {
22010 if ( ! isset( $this->_user_api ) || $flush ) {
22011 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
22012 }
22013
22014 return $this->_user_api;
22015 }
22016
22017 /**
22018 * @author Vova Feldman (@svovaf)
22019 * @since 2.0.0
22020 *
22021 * @param \FS_User $user
22022 *
22023 * @return \FS_Api
22024 */
22025 private function get_api_user_scope_by_user( FS_User $user ) {
22026 return FS_Api::instance(
22027 $this->_module_id,
22028 'user',
22029 $user->id,
22030 $user->public_key,
22031 ! $this->is_live(),
22032 $user->secret_key,
22033 $this->get_sdk_version()
22034 );
22035 }
22036
22037 /**
22038 *
22039 * @author Leo Fajardo (@leorw)
22040 * @since 2.0.0
22041 *
22042 * @param bool $flush
22043 *
22044 * @return FS_Api
22045 */
22046 private function get_current_or_network_user_api_scope( $flush = false ) {
22047 if ( ! $this->_is_network_active ||
22048 ( isset( $this->_user ) && $this->_user instanceof FS_User )
22049 ) {
22050 return $this->get_api_user_scope( $flush );
22051 }
22052
22053 $user = $this->get_current_or_network_user();
22054
22055 $this->_user_api = FS_Api::instance(
22056 $this->_module_id,
22057 'user',
22058 $user->id,
22059 $user->public_key,
22060 ! $this->is_live(),
22061 $user->secret_key,
22062 $this->get_sdk_version()
22063 );
22064
22065 return $this->_user_api;
22066 }
22067
22068 private $_site_api;
22069
22070 /**
22071 *
22072 * @author Vova Feldman (@svovaf)
22073 * @since 1.0.2
22074 *
22075 * @param bool $flush
22076 *
22077 * @return FS_Api
22078 */
22079 private function get_api_site_scope( $flush = false ) {
22080 if ( ! isset( $this->_site_api ) || $flush ) {
22081 $this->_site_api = FS_Api::instance(
22082 $this->_module_id,
22083 'install',
22084 $this->_site->id,
22085 $this->_site->public_key,
22086 ! $this->is_live(),
22087 $this->_site->secret_key,
22088 $this->get_sdk_version()
22089 );
22090 }
22091
22092 return $this->_site_api;
22093 }
22094
22095 private $_plugin_api;
22096
22097 /**
22098 * Get plugin public API scope.
22099 *
22100 * @author Vova Feldman (@svovaf)
22101 * @since 1.0.7
22102 *
22103 * @return FS_Api
22104 */
22105 function get_api_plugin_scope() {
22106 if ( ! isset( $this->_plugin_api ) ) {
22107 $this->_plugin_api = FS_Api::instance(
22108 $this->_module_id,
22109 'plugin',
22110 $this->_plugin->id,
22111 $this->_plugin->public_key,
22112 ! $this->is_live(),
22113 false,
22114 $this->get_sdk_version()
22115 );
22116 }
22117
22118 return $this->_plugin_api;
22119 }
22120
22121 /**
22122 * Get bundle public API scope.
22123 *
22124 * @author Vova Feldman (@svovaf)
22125 * @since 2.3.1
22126 *
22127 * @return FS_Api
22128 */
22129 function get_api_bundle_scope() {
22130 return FS_Api::instance(
22131 $this->get_bundle_id(),
22132 'plugin',
22133 $this->get_bundle_id(),
22134 $this->get_bundle_public_key(),
22135 ! $this->is_live(),
22136 false,
22137 $this->get_sdk_version()
22138 );
22139 }
22140
22141 /**
22142 * Get site API scope object (fallback to public plugin scope when not registered).
22143 *
22144 * @author Vova Feldman (@svovaf)
22145 * @since 1.0.7
22146 *
22147 * @return FS_Api
22148 */
22149 function get_api_site_or_plugin_scope() {
22150 return $this->is_registered() ?
22151 $this->get_api_site_scope() :
22152 $this->get_api_plugin_scope();
22153 }
22154
22155 /**
22156 * Show trial promotional notice (if any trial exist).
22157 *
22158 * @author Vova Feldman (@svovaf)
22159 * @since 1.0.9
22160 *
22161 * @param FS_Plugin_Plan[] $plans
22162 */
22163 function _check_for_trial_plans( $plans ) {
22164 /**
22165 * 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.
22166 *
22167 * @author Vova Feldman (@svovaf)
22168 * @since 2.1.2
22169 */
22170 if ( ! is_array( $plans ) && is_object( $plans ) ) {
22171 $plans = array( $plans );
22172 }
22173
22174 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
22175 $plans = array();
22176 }
22177
22178 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
22179 }
22180
22181 /**
22182 * During trial promotion the "upgrade" submenu item turns to
22183 * "start trial" to encourage the trial. Since we want to keep
22184 * the same menu item handler and there's no robust way to
22185 * add new arguments to the menu item link's querystring,
22186 * use JavaScript to find the menu item and update the href of
22187 * the link.
22188 *
22189 * @author Vova Feldman (@svovaf)
22190 * @since 1.2.1.5
22191 */
22192 function _fix_start_trial_menu_item_url() {
22193 $template_args = array( 'id' => $this->_module_id );
22194 fs_require_template( 'add-trial-to-pricing.php', $template_args );
22195 }
22196
22197 /**
22198 * Check if module is currently in a trial promotion mode.
22199 *
22200 * @author Vova Feldman (@svovaf)
22201 * @since 1.2.2.7
22202 *
22203 * @return bool
22204 */
22205 function is_in_trial_promotion() {
22206 return $this->_admin_notices->has_sticky( 'trial_promotion' );
22207 }
22208
22209 /**
22210 * Show trial promotional notice (if any trial exist).
22211 *
22212 * @author Vova Feldman (@svovaf)
22213 * @since 1.0.9
22214 *
22215 * @return bool If trial notice added.
22216 */
22217 function _add_trial_notice() {
22218 if ( ! $this->is_user_admin() ) {
22219 return false;
22220 }
22221
22222 if ( ! $this->is_user_in_admin() ) {
22223 return false;
22224 }
22225
22226 if ( $this->_is_network_active ) {
22227 if ( fs_is_network_admin() ) {
22228 // Network level trial is disabled at the moment.
22229 return false;
22230 }
22231
22232 if ( ! $this->is_delegated_connection() ) {
22233 // Only delegated sites should support trials.
22234 return false;
22235 }
22236 }
22237
22238 // Check if trial message is already shown.
22239 if ( $this->is_in_trial_promotion() ) {
22240 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
22241
22242 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
22243
22244 return false;
22245 }
22246
22247 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
22248 // Don't show trial if running the premium code, unless running in DEV mode.
22249 return false;
22250 }
22251
22252 if ( ! $this->has_trial_plan() ) {
22253 // No plans with trial.
22254 return false;
22255 }
22256
22257 if ( ! $this->apply_filters( 'show_trial', true ) ) {
22258 // Developer explicitly asked not to show the trial promo.
22259 return false;
22260 }
22261
22262 if ( $this->is_registered() ) {
22263 // Check if trial already utilized.
22264 if ( $this->_site->is_trial_utilized() ) {
22265 return false;
22266 }
22267
22268 if ( $this->is_paying_or_trial() ) {
22269 // Don't show trial if paying or already in trial.
22270 return false;
22271 }
22272 }
22273
22274 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
22275 // If not yet opted-in/skipped, or pending activation, don't show trial.
22276 return false;
22277 }
22278
22279 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
22280 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
22281
22282 // Show promotion if never shown before and 24 hours after initial activation with FS.
22283 if ( ! $was_promotion_shown_before &&
22284 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
22285 ) {
22286 return false;
22287 }
22288
22289 // OR if promotion was shown before, try showing it every 30 days.
22290 if ( $was_promotion_shown_before &&
22291 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
22292 ) {
22293 return false;
22294 }
22295
22296 $trial_period = $this->_trial_days;
22297 $require_payment = $this->_is_trial_require_payment;
22298 $trial_url = $this->get_trial_url();
22299 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
22300
22301 if ( $this->is_registered() ) {
22302 // If opted-in, override trial with up to date data from API.
22303 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
22304 $trial_plans_count = count( $trial_plans );
22305
22306 if ( 0 === $trial_plans_count ) {
22307 // If there's no plans with a trial just exit.
22308 return false;
22309 }
22310
22311 /**
22312 * @var FS_Plugin_Plan $paid_plan
22313 */
22314 $paid_plan = $trial_plans[0];
22315 $require_payment = $paid_plan->is_require_subscription;
22316 $trial_period = $paid_plan->trial_period;
22317
22318 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
22319
22320 if ( $total_paid_plans !== $trial_plans_count ) {
22321 // Not all paid plans have a trial - generate a string of those that have it.
22322 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
22323 $plans_string .= sprintf(
22324 ' <a href="%s">%s</a>',
22325 $trial_url,
22326 $trial_plans[ $i ]->title
22327 );
22328
22329 if ( $i < $trial_plans_count - 2 ) {
22330 $plans_string .= ', ';
22331 } else if ( $i == $trial_plans_count - 2 ) {
22332 $plans_string .= ' and ';
22333 }
22334 }
22335 }
22336 }
22337
22338 $message = sprintf(
22339 $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' ),
22340 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
22341 $plans_string,
22342 $trial_period
22343 );
22344
22345 // "No Credit-Card Required" or "No Commitment for N Days".
22346 $cc_string = $require_payment ?
22347 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
22348 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
22349
22350
22351 // Start trial button.
22352 $button = ' ' . sprintf(
22353 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
22354 $trial_url,
22355 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
22356 );
22357
22358 $this->_admin_notices->add_sticky(
22359 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
22360 'trial_promotion',
22361 '',
22362 'promotion'
22363 );
22364
22365 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
22366
22367 return true;
22368 }
22369
22370 /**
22371 * Lets users/customers know that the product has an affiliate program.
22372 *
22373 * @author Leo Fajardo (@leorw)
22374 * @since 1.2.2.11
22375 *
22376 * @return bool Returns true if the notice has been added.
22377 */
22378 function _add_affiliate_program_notice() {
22379 if ( ! $this->is_user_admin() ) {
22380 return false;
22381 }
22382
22383 if ( ! $this->is_user_in_admin() ) {
22384 return false;
22385 }
22386
22387 // Check if the notice is already shown.
22388 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
22389 return false;
22390 }
22391
22392 if (
22393 // Product has no affiliate program.
22394 ! $this->has_affiliate_program() ||
22395 // User has applied for an affiliate account.
22396 ! empty( $this->_storage->affiliate_application_data )
22397 ) {
22398 return false;
22399 }
22400
22401 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
22402 // Developer explicitly asked not to show the notice about the affiliate program.
22403 return false;
22404 }
22405
22406 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
22407 // If not yet opted in/skipped, or pending activation, don't show the notice.
22408 return false;
22409 }
22410
22411 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
22412 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
22413
22414 /**
22415 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
22416 * activation with FS.
22417 */
22418 if ( $was_notice_shown_before ||
22419 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
22420 ) {
22421 return false;
22422 }
22423
22424 if ( ! $this->is_paying() &&
22425 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
22426 ) {
22427 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
22428 return false;
22429 }
22430
22431 $message = sprintf(
22432 $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' ),
22433 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
22434 $this->get_module_label( true )
22435 );
22436
22437 // HTML code for the "Learn more..." button.
22438 $button = ' ' . sprintf(
22439 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
22440 $this->_get_admin_page_url( 'affiliation' ),
22441 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
22442 );
22443
22444 $this->_admin_notices->add_sticky(
22445 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
22446 'affiliate_program',
22447 '',
22448 'promotion'
22449 );
22450
22451 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
22452
22453 return true;
22454 }
22455
22456 /**
22457 * @author Vova Feldman (@svovaf)
22458 * @since 1.2.1.5
22459 */
22460 function _enqueue_common_css() {
22461 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
22462 // Add basic CSS for admin-notices and menu-item colors.
22463 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
22464 }
22465 }
22466
22467 /**
22468 * @author Leo Fajardo (@leorw)
22469 * @since 1.2.2
22470 */
22471 function _show_theme_activation_optin_dialog() {
22472 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
22473
22474 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
22475 }
22476
22477 /**
22478 * @author Leo Fajardo (@leorw)
22479 * @since 1.2.2
22480 */
22481 function _add_fs_theme_activation_dialog() {
22482 global $pagenow;
22483
22484 if ( 'themes.php' !== $pagenow ) {
22485 return;
22486 }
22487
22488 $vars = array( 'id' => $this->_module_id );
22489 fs_require_once_template( 'connect.php', $vars );
22490 }
22491
22492 /* Action Links
22493 ------------------------------------------------------------------------------------------------------------------*/
22494 private $_action_links_hooked = false;
22495 private $_action_links = array();
22496
22497 /**
22498 * Hook to plugin action links filter.
22499 *
22500 * @author Vova Feldman (@svovaf)
22501 * @since 1.0.0
22502 */
22503 private function hook_plugin_action_links() {
22504 $this->_logger->entrance();
22505
22506 $this->_action_links_hooked = true;
22507
22508 $this->_logger->log( 'Adding action links hooks.' );
22509
22510 // Add action link to settings page.
22511 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
22512 &$this,
22513 '_modify_plugin_action_links_hook'
22514 ), WP_FS__DEFAULT_PRIORITY, 2 );
22515 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
22516 &$this,
22517 '_modify_plugin_action_links_hook'
22518 ), WP_FS__DEFAULT_PRIORITY, 2 );
22519 }
22520
22521 /**
22522 * Add plugin action link.
22523 *
22524 * @author Vova Feldman (@svovaf)
22525 * @since 1.0.0
22526 *
22527 * @param $label
22528 * @param $url
22529 * @param bool $external
22530 * @param int $priority
22531 * @param bool $key
22532 */
22533 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
22534 $this->_logger->entrance();
22535
22536 if ( ! isset( $this->_action_links[ $priority ] ) ) {
22537 $this->_action_links[ $priority ] = array();
22538 }
22539
22540 if ( false === $key ) {
22541 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
22542 }
22543
22544 $this->_action_links[ $priority ][] = array(
22545 'label' => $label,
22546 'href' => $url,
22547 'key' => $key,
22548 'external' => $external
22549 );
22550 }
22551
22552 /**
22553 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
22554 *
22555 * @author Vova Feldman (@svovaf)
22556 * @since 1.0.0
22557 */
22558 function _add_upgrade_action_link() {
22559 $this->_logger->entrance();
22560
22561 $is_activation_mode = $this->is_activation_mode();
22562
22563 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
22564
22565 /**
22566 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
22567 * and "Add-Ons" menus should be added.
22568 *
22569 * @author Leo Fajardo (@leorw)
22570 * @since 2.3.0
22571 */
22572 $add_upgrade_link = (
22573 $add_action_links ||
22574 ( $is_activation_mode && $this->is_only_premium() )
22575 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
22576
22577 $add_addons_link = ( $add_action_links && $this->has_addons() );
22578
22579 if ( ! $add_upgrade_link && ! $add_addons_link ) {
22580 return;
22581 }
22582
22583 if (
22584 $add_upgrade_link &&
22585 $this->is_pricing_page_visible() &&
22586 $this->is_submenu_item_visible( 'pricing' )
22587 ) {
22588 $this->add_plugin_action_link(
22589 $this->get_text_inline( 'Upgrade', 'upgrade' ),
22590 $this->get_upgrade_url(),
22591 false,
22592 7,
22593 'upgrade'
22594 );
22595 }
22596
22597 if (
22598 $add_addons_link &&
22599 $this->has_addons() &&
22600 $this->is_submenu_item_visible( 'addons' )
22601 ) {
22602 $this->add_plugin_action_link(
22603 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
22604 $this->_get_admin_page_url( 'addons' ),
22605 false,
22606 9,
22607 'addons'
22608 );
22609 }
22610 }
22611
22612 /**
22613 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
22614 *
22615 * @author Leo Fajardo (@leorw)
22616 * @since 1.1.9
22617 */
22618 function _add_license_action_link() {
22619 $this->_logger->entrance();
22620
22621 if ( ! self::is_ajax() ) {
22622 // Inject license activation dialog UI and client side code.
22623 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
22624 }
22625
22626 $link_text = $this->is_free_plan() ?
22627 $this->get_text_inline( 'Activate License', 'activate-license' ) :
22628 $this->get_text_inline( 'Change License', 'change-license' );
22629
22630 $this->add_plugin_action_link(
22631 $link_text,
22632 '#',
22633 false,
22634 11,
22635 ( 'activate-license ' . $this->get_unique_affix() )
22636 );
22637 }
22638
22639 /**
22640 * @author Leo Fajardo (@leorw)
22641 * @since 2.0.2
22642 */
22643 function _add_premium_version_upgrade_selection_action() {
22644 $this->_logger->entrance();
22645
22646 if ( ! self::is_ajax() ) {
22647 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
22648 }
22649 }
22650
22651 /**
22652 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
22653 *
22654 * @author Leo Fajardo (@leorw)
22655 * @since 1.2.1.5
22656 */
22657 function _add_tracking_links() {
22658 if ( ! current_user_can( 'manage_options' ) ) {
22659 return;
22660 }
22661
22662 $this->_logger->entrance();
22663
22664 /**
22665 * @author Vova Feldman (@svovaf)
22666 * @since 2.3.2 Allow opting out from usage-tracking for paid products too by giving the appropriate warning letting the user know the automatic updates mechanism cannot function without an ongoing connection to the licensing and updates engine.
22667 */
22668 /*if ( $this->is_premium() ) {
22669 // Don't add opt-in/out for premium code base.
22670 return;
22671 }*/
22672
22673 if ( $this->is_only_premium() && $this->is_free_plan() ) {
22674 // 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.
22675 return;
22676 }
22677
22678 if ( fs_is_network_admin() ) {
22679 if ( ! $this->_is_network_active ) {
22680 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
22681 return;
22682 } else if ( $this->is_network_delegated_connection() ) {
22683 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
22684 return;
22685 }
22686 } else {
22687 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
22688 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
22689 return;
22690 }
22691 }
22692
22693 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
22694 if ( ! $this->is_registered() && $this->is_anonymous() ) {
22695 $this->connect_again();
22696
22697 return;
22698 }
22699 }
22700
22701 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
22702 ( $this->is_theme() && ! self::is_themes_page() )
22703 ) {
22704 // Only show tracking links on the plugins and themes pages.
22705 return;
22706 }
22707
22708 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
22709 if ( ! $this->is_enable_anonymous() ) {
22710 // If opted in and tracking is allowed, don't allow to opt out if anonymous mode is disabled.
22711 return;
22712 }
22713
22714 if ( ! $this->is_free_plan() ) {
22715 // Don't allow to opt out if running in paid plan.
22716 return;
22717 }
22718 }
22719
22720 if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) {
22721 return;
22722 }
22723
22724 if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) {
22725 return;
22726 }
22727
22728 if ( $this->add_ajax_action( 'update_tracking_permission', array( &$this, '_update_tracking_permission_callback' ) ) ) {
22729 return;
22730 }
22731
22732 $link_text_id = '';
22733 $url = '#';
22734
22735 if ( $this->is_registered() ) {
22736 if ( $this->is_tracking_allowed() ) {
22737 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
22738 } else {
22739 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
22740 }
22741 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
22742 /**
22743 * Show opt-in link only if skipped or in activation mode.
22744 */
22745 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
22746
22747 $params = ! $this->is_anonymous() ?
22748 array() :
22749 array(
22750 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
22751 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
22752 );
22753
22754 $url = $this->get_activation_url( $params );
22755 }
22756
22757 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
22758
22759 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
22760 $this->add_plugin_action_link(
22761 $link_text_id,
22762 $url,
22763 false,
22764 13,
22765 "opt-in-or-opt-out {$this->_slug}"
22766 );
22767 }
22768 }
22769
22770 /**
22771 * Get the URL of the page that should be loaded right after the plugin activation.
22772 *
22773 * @author Vova Feldman (@svovaf)
22774 * @since 1.1.7.4
22775 *
22776 * @return string
22777 */
22778 function get_after_plugin_activation_redirect_url() {
22779 $url = false;
22780
22781 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
22782 $first_time_path = $this->_menu->get_first_time_path(
22783 fs_is_network_admin() && $this->_is_network_active
22784 );
22785
22786 if ( $this->is_activation_mode() ) {
22787 $url = $this->get_activation_url();
22788 } else if ( ! empty( $first_time_path ) ) {
22789 $url = $first_time_path;
22790 } else {
22791 $page = '';
22792 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
22793 if ( $this->is_network_registered() ) {
22794 $page = 'account';
22795 } else if ( $this->is_network_anonymous() ) {
22796 $this->maybe_set_slug_and_network_menu_exists_flag();
22797 }
22798 }
22799
22800 $url = $this->_get_admin_page_url( $page );
22801 }
22802 } else {
22803 $plugin_fs = false;
22804
22805 if ( $this->is_parent_plugin_installed() ) {
22806 $plugin_fs = self::get_parent_instance();
22807 }
22808
22809 if ( is_object( $plugin_fs ) ) {
22810 if ( ! $plugin_fs->is_registered() ) {
22811 // Forward to parent plugin connect when parent not registered.
22812 $url = $plugin_fs->get_activation_url();
22813 } else {
22814 // Forward to account page.
22815 $url = $plugin_fs->_get_admin_page_url( 'account' );
22816 }
22817 }
22818 }
22819
22820 return $url;
22821 }
22822
22823 /**
22824 * Forward page to activation page.
22825 *
22826 * @author Vova Feldman (@svovaf)
22827 * @since 1.0.3
22828 */
22829 function _redirect_on_activation_hook() {
22830 $url = $this->get_after_plugin_activation_redirect_url();
22831
22832 if ( is_string( $url ) ) {
22833 fs_redirect( $url );
22834 }
22835 }
22836
22837 /**
22838 * Modify plugin's page action links collection.
22839 *
22840 * @author Vova Feldman (@svovaf)
22841 * @since 1.0.0
22842 *
22843 * @param array $links
22844 * @param $file
22845 *
22846 * @return array
22847 */
22848 function _modify_plugin_action_links_hook( $links, $file ) {
22849 $this->_logger->entrance();
22850
22851 $passed_deactivate = false;
22852 $deactivate_link = '';
22853 $before_deactivate = array();
22854 $after_deactivate = array();
22855 foreach ( $links as $key => $link ) {
22856 if ( 'deactivate' === $key ) {
22857 $deactivate_link = $link;
22858 $passed_deactivate = true;
22859 continue;
22860 }
22861
22862 if ( ! $passed_deactivate ) {
22863 $before_deactivate[ $key ] = $link;
22864 } else {
22865 $after_deactivate[ $key ] = $link;
22866 }
22867 }
22868
22869 ksort( $this->_action_links );
22870
22871 foreach ( $this->_action_links as $new_links ) {
22872 foreach ( $new_links as $link ) {
22873 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank"' : '' ) . '>' . $link['label'] . '</a>';
22874 }
22875 }
22876
22877 if ( ! empty( $deactivate_link ) ) {
22878 /**
22879 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
22880 *
22881 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
22882 */
22883 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
22884
22885 // Append deactivation link.
22886 $before_deactivate['deactivate'] = $deactivate_link;
22887 }
22888
22889 return array_merge( $before_deactivate, $after_deactivate );
22890 }
22891
22892 /**
22893 * Adds admin message.
22894 *
22895 * @author Vova Feldman (@svovaf)
22896 * @since 1.0.4
22897 *
22898 * @param string $message
22899 * @param string $title
22900 * @param string $type
22901 */
22902 function add_admin_message( $message, $title = '', $type = 'success' ) {
22903 $this->_admin_notices->add( $message, $title, $type );
22904 }
22905
22906 /**
22907 * Adds sticky admin message.
22908 *
22909 * @author Vova Feldman (@svovaf)
22910 * @since 1.1.0
22911 *
22912 * @param string $message
22913 * @param string $id
22914 * @param string $title
22915 * @param string $type
22916 */
22917 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
22918 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
22919 }
22920
22921 /**
22922 * Check if the paid version of the module is installed.
22923 *
22924 * @author Vova Feldman (@svovaf)
22925 * @since 2.2.0
22926 *
22927 * @return bool
22928 */
22929 private function is_premium_version_installed() {
22930 $premium_plugin_basename = $this->premium_plugin_basename();
22931 $premium_plugin = get_plugins( '/' . dirname( $premium_plugin_basename ) );
22932
22933 return ! empty( $premium_plugin );
22934 }
22935
22936 /**
22937 * Helper function that returns the final steps for the upgrade completion.
22938 *
22939 * If the module is already running the premium code, returns an empty string.
22940 *
22941 * @author Vova Feldman (@svovaf)
22942 * @since 1.2.1
22943 *
22944 * @param string $plan_title
22945 *
22946 * @return string
22947 */
22948 private function get_complete_upgrade_instructions( $plan_title = '' ) {
22949 $this->_logger->entrance();
22950
22951 $activate_license_string = $this->get_license_network_activation_notice();
22952
22953 if ( ! $this->has_premium_version() || $this->is_premium() ) {
22954 return '' . $activate_license_string;
22955 }
22956
22957 if ( empty( $plan_title ) ) {
22958 $plan_title = $this->get_plan_title();
22959 }
22960
22961 if ( $this->is_premium_version_installed() ) {
22962 /**
22963 * If the premium version is already installed, instead of showing the installation instructions,
22964 * tell the current user to activate it.
22965 *
22966 * @author Leo Fajardo (@leorw)
22967 * @since 2.2.1
22968 */
22969 $premium_plugin_basename = $this->premium_plugin_basename();
22970
22971 return sprintf(
22972 /* translators: %1$s: Product title; %2$s: Plan title */
22973 $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' ),
22974 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
22975 $plan_title,
22976 sprintf(
22977 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
22978 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_plugin_basename, 'activate-plugin_' . $premium_plugin_basename ),
22979 esc_html( sprintf(
22980 /* translators: %s: Plan title */
22981 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
22982 $plan_title
22983 ) )
22984 )
22985 );
22986 } else {
22987 // @since 1.2.1.5 The free version is auto deactivated.
22988 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
22989 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
22990 '';
22991
22992 return sprintf(
22993 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank">%s</a>).</li></ol>',
22994 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
22995 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
22996 $this->get_latest_download_link( sprintf(
22997 /* translators: %s: Plan title */
22998 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
22999 $plan_title
23000 ) ),
23001 $deactivation_step,
23002 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
23003 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/upload-wp-' . $this->_module_type . 's' ),
23004 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
23005 );
23006 }
23007 }
23008
23009 /**
23010 * @author Leo Fajardo (@leorw)
23011 * @since 2.1.0
23012 *
23013 * @param string $url
23014 * @param array $request
23015 */
23016 private static function enrich_request_for_debug( &$url, &$request ) {
23017 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
23018 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
23019 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
23020
23021 $request['cookies'] = array(
23022 new WP_Http_Cookie( array(
23023 'name' => 'XDEBUG_SESSION',
23024 'value' => 'PHPSTORM',
23025 ) )
23026 );
23027 }
23028 }
23029
23030 /**
23031 * @author Leo Fajardo (@leorw)
23032 * @since 2.1.0
23033 *
23034 * @param string $url
23035 * @param array $request
23036 * @param int $success_cache_expiration
23037 * @param int $failure_cache_expiration
23038 * @param bool $maybe_enrich_request_for_debug
23039 *
23040 * @return WP_Error|array
23041 */
23042 static function safe_remote_post(
23043 &$url,
23044 $request,
23045 $success_cache_expiration = 0,
23046 $failure_cache_expiration = 0,
23047 $maybe_enrich_request_for_debug = true
23048 ) {
23049 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
23050
23051 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
23052
23053 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
23054 get_transient( $cache_key ) :
23055 false;
23056
23057 if ( false === $response ) {
23058 if ( $maybe_enrich_request_for_debug ) {
23059 self::enrich_request_for_debug( $url, $request );
23060 }
23061
23062 $response = wp_remote_post( $url, $request );
23063
23064 if ( $response instanceof WP_Error ) {
23065 if ( 'https://' === substr( $url, 0, 8 ) &&
23066 isset( $response->errors ) &&
23067 isset( $response->errors['http_request_failed'] )
23068 ) {
23069 $http_error = strtolower( $response->errors['http_request_failed'][0] );
23070
23071 if ( false !== strpos( $http_error, 'ssl' ) ||
23072 false !== strpos( $http_error, 'curl error 35' )
23073 ) {
23074 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
23075 $url = 'http://' . substr( $url, 8 );
23076
23077 $request['timeout'] = 15;
23078 $response = wp_remote_post( $url, $request );
23079 }
23080 }
23081 }
23082
23083 if ( false !== $cache_key ) {
23084 set_transient(
23085 $cache_key,
23086 $response,
23087 ( ( $response instanceof WP_Error ) ?
23088 $failure_cache_expiration :
23089 $success_cache_expiration )
23090 );
23091 }
23092 }
23093
23094 return $response;
23095 }
23096
23097 /**
23098 * This method is used to enrich the after upgrade notice instructions when the upgraded
23099 * license cannot be activated network wide (license quota isn't large enough).
23100 *
23101 * @author Vova Feldman (@svovaf)
23102 * @since 2.0.0
23103 *
23104 * @return string
23105 */
23106 private function get_license_network_activation_notice() {
23107 if ( ! $this->_is_network_active ) {
23108 // Module isn't network level activated.
23109 return '';
23110 }
23111
23112 if ( ! fs_is_network_admin() ) {
23113 // Not network level admin.
23114 return '';
23115 }
23116
23117 if ( get_blog_count() == 1 ) {
23118 // There's only a single site in the network so if there's a context license it was already activated.
23119 return '';
23120 }
23121
23122 if ( ! is_object( $this->_license ) ) {
23123 // No context license.
23124 return '';
23125 }
23126
23127 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
23128 // 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).
23129 return '';
23130 }
23131
23132 if ( $this->can_activate_license_on_network( $this->_license ) ) {
23133 // 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).
23134 return '';
23135 }
23136
23137 return sprintf(
23138 $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' ),
23139 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
23140 '</a>'
23141 );
23142 }
23143
23144 /**
23145 * @author Vova Feldman (@svovaf)
23146 * @since 1.2.1.7
23147 *
23148 * @param string $key
23149 *
23150 * @return string
23151 */
23152 function get_text( $key ) {
23153 return fs_text( $key, $this->_slug );
23154 }
23155
23156 /**
23157 * @author Vova Feldman (@svovaf)
23158 * @since 1.2.3
23159 *
23160 * @param string $text Translatable string.
23161 * @param string $key String key for overrides.
23162 *
23163 * @return string
23164 */
23165 function get_text_inline( $text, $key = '' ) {
23166 return _fs_text_inline( $text, $key, $this->_slug );
23167 }
23168
23169 /**
23170 * @author Vova Feldman (@svovaf)
23171 * @since 1.2.3
23172 *
23173 * @param string $text Translatable string.
23174 * @param string $context Context information for the translators.
23175 * @param string $key String key for overrides.
23176 *
23177 * @return string
23178 */
23179 function get_text_x_inline( $text, $context, $key ) {
23180 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
23181 }
23182
23183 /**
23184 * @author Vova Feldman (@svovaf)
23185 * @since 1.2.3
23186 *
23187 * @param string $text Translatable string.
23188 * @param string $key String key for overrides.
23189 *
23190 * @return string
23191 */
23192 function esc_html_inline( $text, $key ) {
23193 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
23194 }
23195
23196 #----------------------------------------------------------------------------------
23197 #region Versioning
23198 #----------------------------------------------------------------------------------
23199
23200 /**
23201 * Check if Freemius in SDK upgrade mode.
23202 *
23203 * @author Vova Feldman (@svovaf)
23204 * @since 1.0.9
23205 *
23206 * @return bool
23207 */
23208 function is_sdk_upgrade_mode() {
23209 return isset( $this->_storage->sdk_upgrade_mode ) ?
23210 $this->_storage->sdk_upgrade_mode :
23211 false;
23212 }
23213
23214 /**
23215 * Turn SDK upgrade mode off.
23216 *
23217 * @author Vova Feldman (@svovaf)
23218 * @since 1.0.9
23219 */
23220 function set_sdk_upgrade_complete() {
23221 $this->_storage->sdk_upgrade_mode = false;
23222 }
23223
23224 /**
23225 * Check if plugin upgrade mode.
23226 *
23227 * @author Vova Feldman (@svovaf)
23228 * @since 1.0.9
23229 *
23230 * @return bool
23231 */
23232 function is_plugin_upgrade_mode() {
23233 return isset( $this->_storage->plugin_upgrade_mode ) ?
23234 $this->_storage->plugin_upgrade_mode :
23235 false;
23236 }
23237
23238 /**
23239 * Turn plugin upgrade mode off.
23240 *
23241 * @author Vova Feldman (@svovaf)
23242 * @since 1.0.9
23243 */
23244 function set_plugin_upgrade_complete() {
23245 $this->_storage->plugin_upgrade_mode = false;
23246 }
23247
23248 #endregion
23249
23250 #----------------------------------------------------------------------------------
23251 #region Permissions
23252 #----------------------------------------------------------------------------------
23253
23254 /**
23255 * Check if specific permission requested.
23256 *
23257 * @author Vova Feldman (@svovaf)
23258 * @since 1.1.6
23259 *
23260 * @param string $permission
23261 *
23262 * @return bool
23263 */
23264 function is_permission_requested( $permission ) {
23265 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
23266 }
23267
23268 #endregion
23269
23270 #----------------------------------------------------------------------------------
23271 #region Auto Activation
23272 #----------------------------------------------------------------------------------
23273
23274 /**
23275 * Hints the SDK if running an auto-installation.
23276 *
23277 * @var bool
23278 */
23279 private $_isAutoInstall = false;
23280
23281 /**
23282 * After upgrade callback to install and auto activate a plugin.
23283 * This code will only be executed on explicit request from the user,
23284 * following the practice Jetpack are using with their theme installations.
23285 *
23286 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
23287 *
23288 * @author Vova Feldman (@svovaf)
23289 * @since 1.2.1.7
23290 */
23291 function _install_premium_version_ajax_action() {
23292 $this->_logger->entrance();
23293
23294 $this->check_ajax_referer( 'install_premium_version' );
23295
23296 if ( ! $this->is_registered() ) {
23297 // Not registered.
23298 self::shoot_ajax_failure( array(
23299 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
23300 'code' => 'premium_installed',
23301 ) );
23302 }
23303
23304 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
23305
23306 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
23307 // Invalid ID.
23308 self::shoot_ajax_failure( array(
23309 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
23310 'code' => 'invalid_module_id',
23311 ) );
23312 }
23313
23314 if ( $plugin_id == $this->get_id() ) {
23315 if ( $this->is_premium() ) {
23316 // Already using the premium code version.
23317 self::shoot_ajax_failure( array(
23318 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
23319 'code' => 'premium_installed',
23320 ) );
23321 }
23322 if ( ! $this->can_use_premium_code() ) {
23323 // Don't have access to the premium code.
23324 self::shoot_ajax_failure( array(
23325 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
23326 'code' => 'invalid_license',
23327 ) );
23328 }
23329 if ( ! $this->has_release_on_freemius() ) {
23330 // Plugin is a serviceware, no premium code version.
23331 self::shoot_ajax_failure( array(
23332 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
23333 'code' => 'premium_version_missing',
23334 ) );
23335 }
23336 } else {
23337 $addon = $this->get_addon( $plugin_id );
23338
23339 if ( ! is_object( $addon ) ) {
23340 // Invalid add-on ID.
23341 self::shoot_ajax_failure( array(
23342 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
23343 'code' => 'invalid_module_id',
23344 ) );
23345 }
23346
23347 if ( $this->is_addon_activated( $plugin_id, true ) ) {
23348 // Premium add-on version is already activated.
23349 self::shoot_ajax_failure( array(
23350 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
23351 'code' => 'premium_installed',
23352 ) );
23353 }
23354 }
23355
23356 $this->_isAutoInstall = true;
23357
23358 // Try to install and activate.
23359 $updater = FS_Plugin_Updater::instance( $this );
23360 $result = $updater->install_and_activate_plugin( $plugin_id );
23361
23362 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
23363 self::shoot_ajax_failure( array(
23364 'message' => $result['message'],
23365 'code' => $result['code'],
23366 ) );
23367 }
23368
23369 self::shoot_ajax_success( $result );
23370 }
23371
23372 /**
23373 * Displays module activation dialog box after a successful upgrade
23374 * where the user explicitly requested to auto download and install
23375 * the premium version.
23376 *
23377 * @author Vova Feldman (@svovaf)
23378 * @since 1.2.1.7
23379 */
23380 function _add_auto_installation_dialog_box() {
23381 $this->_logger->entrance();
23382
23383 if ( ! $this->is_registered() ) {
23384 // Not registered.
23385 return;
23386 }
23387
23388 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
23389
23390 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
23391 // Invalid module ID.
23392 return;
23393 }
23394
23395 if ( $plugin_id == $this->get_id() ) {
23396 if ( $this->is_premium() ) {
23397 // Already using the premium code version.
23398 return;
23399 }
23400 if ( ! $this->can_use_premium_code() ) {
23401 // Don't have access to the premium code.
23402 return;
23403 }
23404 if ( ! $this->has_release_on_freemius() ) {
23405 // Plugin is a serviceware, no premium code version.
23406 return;
23407 }
23408 } else {
23409 $addon = $this->get_addon( $plugin_id );
23410
23411 if ( ! is_object( $addon ) ) {
23412 // Invalid add-on ID.
23413 return;
23414 }
23415
23416 if ( $this->is_addon_activated( $plugin_id, true ) ) {
23417 // Premium add-on version is already activated.
23418 return;
23419 }
23420 }
23421
23422 $vars = array(
23423 'id' => $this->_module_id,
23424 'target_module_id' => $plugin_id,
23425 'slug' => $this->_slug,
23426 );
23427
23428 fs_require_template( 'auto-installation.php', $vars );
23429 }
23430
23431 #endregion
23432
23433 #--------------------------------------------------------------------------------
23434 #region Tabs Integration
23435 #--------------------------------------------------------------------------------
23436
23437 #region Module's Original Tabs
23438
23439 /**
23440 * Inject a JavaScript logic to capture the theme tabs HTML.
23441 *
23442 * @author Vova Feldman (@svovaf)
23443 * @since 1.2.2.7
23444 */
23445 function _tabs_capture() {
23446 $this->_logger->entrance();
23447
23448 if ( ! $this->is_product_settings_page() ||
23449 ! $this->is_matching_url( $this->main_menu_url() )
23450 ) {
23451 return;
23452 }
23453
23454 $params = array(
23455 'id' => $this->_module_id,
23456 );
23457
23458 fs_require_once_template( 'tabs-capture-js.php', $params );
23459 }
23460
23461 /**
23462 * Cache theme's tabs HTML for a week. The cache will also be set as expired
23463 * after version and type (free/premium) changes, in addition to the week period.
23464 *
23465 * @author Vova Feldman (@svovaf)
23466 * @since 1.2.2.7
23467 */
23468 function _store_tabs_ajax_action() {
23469 $this->_logger->entrance();
23470
23471 $this->check_ajax_referer( 'store_tabs' );
23472
23473 // Init filesystem if not yet initiated.
23474 WP_Filesystem();
23475
23476 // Get POST body HTML data.
23477 global $wp_filesystem;
23478 $tabs_html = $wp_filesystem->get_contents( "php://input" );
23479
23480 if ( is_string( $tabs_html ) ) {
23481 $tabs_html = trim( $tabs_html );
23482 }
23483
23484 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
23485 self::shoot_ajax_failure();
23486 }
23487
23488 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
23489
23490 self::shoot_ajax_success();
23491 }
23492
23493 /**
23494 * Cache theme's settings page custom styles. The cache will also be set as expired
23495 * after version and type (free/premium) changes, in addition to the week period.
23496 *
23497 * @author Vova Feldman (@svovaf)
23498 * @since 1.2.2.7
23499 */
23500 function _store_tabs_styles() {
23501 $this->_logger->entrance();
23502
23503 if ( ! $this->is_product_settings_page() ||
23504 ! $this->is_matching_url( $this->main_menu_url() )
23505 ) {
23506 return;
23507 }
23508
23509 $wp_styles = wp_styles();
23510
23511 $theme_styles_url = get_template_directory_uri();
23512
23513 $stylesheets = array();
23514 foreach ( $wp_styles->queue as $handler ) {
23515 if ( fs_starts_with( $handler, 'fs_' ) ) {
23516 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
23517 continue;
23518 }
23519
23520 /**
23521 * @var _WP_Dependency $stylesheet
23522 */
23523 $stylesheet = $wp_styles->registered[ $handler ];
23524
23525 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
23526 $stylesheets[] = $stylesheet->src;
23527 }
23528 }
23529
23530 if ( ! empty( $stylesheets ) ) {
23531 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
23532 }
23533 }
23534
23535 /**
23536 * Check if module's original settings page has any tabs.
23537 *
23538 * @author Vova Feldman (@svovaf)
23539 * @since 1.2.2.7
23540 *
23541 * @return bool
23542 */
23543 private function has_tabs() {
23544 return $this->_cache->has( 'tabs' );
23545 }
23546
23547 /**
23548 * Get module's settings page HTML content, starting
23549 * from the beginning of the <div class="wrap"> element,
23550 * until the tabs HTML (including).
23551 *
23552 * @author Vova Feldman (@svovaf)
23553 * @since 1.2.2.7
23554 *
23555 * @return string
23556 */
23557 private function get_tabs_html() {
23558 $this->_logger->entrance();
23559
23560 return $this->_cache->get( 'tabs' );
23561 }
23562
23563 /**
23564 * Check if page should include tabs.
23565 *
23566 * @author Vova Feldman (@svovaf)
23567 * @since 1.2.2.7
23568 *
23569 * @return bool
23570 */
23571 private function should_page_include_tabs() {
23572 if ( ! $this->has_settings_menu() ) {
23573 // Don't add tabs if no settings at all.
23574 return false;
23575 }
23576
23577 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
23578 // Only add tabs to themes for now.
23579 return false;
23580 }
23581
23582 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
23583 // Only add tabs to monetizing themes.
23584 return false;
23585 }
23586
23587 if ( ! $this->is_product_settings_page() ) {
23588 // Only add tabs if browsing one of the product's setting pages.
23589 return false;
23590 }
23591
23592 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
23593 // Don't include tabs in the activation page.
23594 return false;
23595 }
23596
23597 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
23598 // Don't add tabs on checkout page, we want to reduce distractions
23599 // as much as possible.
23600 return false;
23601 }
23602
23603 return true;
23604 }
23605
23606 /**
23607 * Add the tabs HTML before the setting's page content and
23608 * enqueue any required stylesheets.
23609 *
23610 * @author Vova Feldman (@svovaf)
23611 * @since 1.2.2.7
23612 *
23613 * @return bool If tabs were included.
23614 */
23615 function _add_tabs_before_content() {
23616 $this->_logger->entrance();
23617
23618 if ( ! $this->should_page_include_tabs() ) {
23619 return false;
23620 }
23621
23622 /**
23623 * Enqueue the original stylesheets that are included in the
23624 * theme settings page. That way, if the theme settings has
23625 * some custom _styled_ content above the tabs UI, this
23626 * will make sure that the styling is preserved.
23627 */
23628 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
23629 if ( is_array( $stylesheets ) ) {
23630 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
23631 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
23632 }
23633 }
23634
23635 // Cut closing </div> tag.
23636 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
23637
23638 return true;
23639 }
23640
23641 /**
23642 * Add the tabs closing HTML after the setting's page content.
23643 *
23644 * @author Vova Feldman (@svovaf)
23645 * @since 1.2.2.7
23646 *
23647 * @return bool If tabs closing HTML was included.
23648 */
23649 function _add_tabs_after_content() {
23650 $this->_logger->entrance();
23651
23652 if ( ! $this->should_page_include_tabs() ) {
23653 return false;
23654 }
23655
23656 echo '</div>';
23657
23658 return true;
23659 }
23660
23661 #endregion
23662
23663 /**
23664 * Add in-page JavaScript to inject the Freemius tabs into
23665 * the module's setting tabs section.
23666 *
23667 * @author Vova Feldman (@svovaf)
23668 * @since 1.2.2.7
23669 */
23670 function _add_freemius_tabs() {
23671 $this->_logger->entrance();
23672
23673 if ( ! $this->should_page_include_tabs() ) {
23674 return;
23675 }
23676
23677 $params = array( 'id' => $this->_module_id );
23678 fs_require_once_template( 'tabs.php', $params );
23679 }
23680
23681 #endregion
23682
23683 #--------------------------------------------------------------------------------
23684 #region Customizer Integration for Themes
23685 #--------------------------------------------------------------------------------
23686
23687 /**
23688 * @author Vova Feldman (@svovaf)
23689 * @since 1.2.2.7
23690 *
23691 * @param WP_Customize_Manager $customizer
23692 */
23693 function _customizer_register( $customizer ) {
23694 $this->_logger->entrance();
23695
23696 if ( $this->is_pricing_page_visible() ) {
23697 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
23698
23699 $customizer->add_section( 'freemius_upsell', array(
23700 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
23701 'priority' => 1,
23702 ) );
23703 $customizer->add_setting( 'freemius_upsell', array(
23704 'sanitize_callback' => 'esc_html',
23705 ) );
23706
23707 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
23708 'fs' => $this,
23709 'section' => 'freemius_upsell',
23710 'priority' => 100,
23711 ) ) );
23712 }
23713
23714 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
23715 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
23716
23717 // Main Documentation Link In Customizer Root.
23718 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
23719 'fs' => $this,
23720 'priority' => 1000,
23721 ) ) );
23722 }
23723 }
23724
23725 #endregion
23726
23727 /**
23728 * If the theme has a paid version, add some custom
23729 * styling to the theme's premium version (if exists)
23730 * to highlight that it's the premium version of the
23731 * same theme, making it easier for identification
23732 * after the user upgrades and upload it to the site.
23733 *
23734 * @author Vova Feldman (@svovaf)
23735 * @since 1.2.2.7
23736 */
23737 function _style_premium_theme() {
23738 $this->_logger->entrance();
23739
23740 if ( ! self::is_themes_page() ) {
23741 // Only include in the themes page.
23742 return;
23743 }
23744
23745 if ( ! $this->has_paid_plan() ) {
23746 // Only include if has any paid plans.
23747 return;
23748 }
23749
23750 $params = null;
23751 fs_require_once_template( '/js/jquery.content-change.php', $params );
23752
23753 $params = array(
23754 'slug' => $this->_slug,
23755 'id' => $this->_module_id,
23756 );
23757
23758 fs_require_template( '/js/style-premium-theme.php', $params );
23759 }
23760
23761 /**
23762 * This method will return the absolute URL of the module's local icon.
23763 *
23764 * When you are running your plugin or theme on a **localhost** environment, if the icon
23765 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
23766 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
23767 * If an icon is found, this method will automatically attempt to download the icon and store it
23768 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
23769 *
23770 * It's important to mention that this method is NOT phoning home since the developer will deploy
23771 * the product with the local icon in the assets folder. The download process just simplifies
23772 * the process for the developer.
23773 *
23774 * @author Vova Feldman (@svovaf)
23775 * @since 2.0.0
23776 *
23777 * @return string
23778 */
23779 function get_local_icon_url() {
23780 global $fs_active_plugins;
23781
23782 /**
23783 * @since 1.1.7.5
23784 */
23785 $local_path = $this->apply_filters( 'plugin_icon', false );
23786
23787 if ( is_string( $local_path ) ) {
23788 $icons = array( $local_path );
23789 } else {
23790 $img_dir = WP_FS__DIR_IMG;
23791
23792 // Locate the main assets folder.
23793 if ( 1 < count( $fs_active_plugins->plugins ) ) {
23794 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
23795
23796 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
23797 if ( $data->plugin_path == $this->get_plugin_basename() ) {
23798 $img_dir = $plugin_or_theme_img_dir
23799 . '/'
23800 /**
23801 * The basename will be `themes` or the basename of a custom themes directory.
23802 *
23803 * @author Leo Fajardo (@leorw)
23804 * @since 2.2.3
23805 */
23806 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
23807 . '/assets/img';
23808
23809 break;
23810 }
23811 }
23812 }
23813
23814 // Try to locate the icon in the assets folder.
23815 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
23816
23817 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
23818 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
23819 $icons = array(
23820 fs_normalize_path( $img_dir . '/theme-icon.png' )
23821 );
23822 } else {
23823 $icon_found = false;
23824 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
23825
23826 if ( ! function_exists( 'get_filesystem_method' ) ) {
23827 require_once ABSPATH . 'wp-admin/includes/file.php';
23828 }
23829
23830 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
23831
23832 /**
23833 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
23834 *
23835 * This code will only be executed once during the testing
23836 * of the plugin in a local environment. The plugin icon file WILL
23837 * already exist in the assets folder when the plugin is deployed to
23838 * the repository.
23839 */
23840 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
23841 // Fetch icon from Freemius.
23842 $icon = $this->fetch_remote_icon_url();
23843
23844 // Fetch icon from WordPress.org.
23845 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
23846 if ( ! function_exists( 'plugins_api' ) ) {
23847 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
23848 }
23849
23850 $plugin_information = plugins_api( 'plugin_information', array(
23851 'slug' => $this->_slug,
23852 'fields' => array(
23853 'sections' => false,
23854 'tags' => false,
23855 'icons' => true
23856 )
23857 ) );
23858
23859 if (
23860 ! is_wp_error( $plugin_information )
23861 && isset( $plugin_information->icons )
23862 && ! empty( $plugin_information->icons )
23863 ) {
23864 /**
23865 * Get the smallest icon.
23866 *
23867 * @author Leo Fajardo (@leorw)
23868 * @since 1.2.2
23869 */
23870 $icon = end( $plugin_information->icons );
23871 }
23872 }
23873
23874 if ( ! empty( $icon ) ) {
23875 if ( 0 !== strpos( $icon, 'http' ) ) {
23876 $icon = 'http:' . $icon;
23877 }
23878
23879 /**
23880 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
23881 *
23882 * @author Leo Fajardo (@leorw)
23883 * @since 1.2.2
23884 */
23885 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
23886
23887 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
23888
23889 // Try to download the icon.
23890 $icon_found = fs_download_image( $icon, $local_path );
23891 }
23892 }
23893
23894 if ( ! $icon_found ) {
23895 // No icons found, fallback to default icon.
23896 if ( $have_write_permissions ) {
23897 // If have write permissions, copy default icon.
23898 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
23899 } else {
23900 // If doesn't have write permissions, use default icon path.
23901 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
23902 }
23903 }
23904
23905 $icons = array( $local_path );
23906 }
23907 }
23908 }
23909
23910 $icon_dir = dirname( $icons[0] );
23911
23912 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
23913 }
23914
23915 /**
23916 * Fetch module's extended info.
23917 *
23918 * @author Vova Feldman (@svovaf)
23919 * @since 2.0.0
23920 *
23921 * @return object|mixed
23922 */
23923 private function fetch_module_info() {
23924 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
23925 }
23926
23927 /**
23928 * Fetch module's remote icon URL.
23929 *
23930 * @author Vova Feldman (@svovaf)
23931 * @since 2.0.0
23932 *
23933 * @return string
23934 */
23935 function fetch_remote_icon_url() {
23936 $info = $this->fetch_module_info();
23937
23938 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
23939 $info->icon :
23940 '';
23941 }
23942
23943 #--------------------------------------------------------------------------------
23944 #region GDPR
23945 #--------------------------------------------------------------------------------
23946
23947 /**
23948 * @author Leo Fajardo (@leorw)
23949 * @since 2.1.0
23950 *
23951 * @return bool
23952 */
23953 function fetch_and_store_current_user_gdpr_anonymously() {
23954 $pong = $this->ping( null, true );
23955
23956 if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
23957 return false;
23958 } else {
23959 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
23960
23961 return $pong->is_gdpr_required;
23962 }
23963 }
23964
23965 /**
23966 * @author Leo Fajardo (@leorw)
23967 * @since 2.1.0
23968 *
23969 * @param array $user_plugins
23970 *
23971 * @return string
23972 */
23973 private function get_gdpr_admin_notice_string( $user_plugins ) {
23974 $this->_logger->entrance();
23975
23976 $addons = self::get_all_addons();
23977
23978 foreach ( $user_plugins as $user_plugin ) {
23979 $has_addons = isset( $addons[ $user_plugin->id ] );
23980
23981 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
23982 if ( $this->_module_id == $user_plugin->id ) {
23983 $addons = $this->get_addons();
23984 $has_addons = ( ! empty( $addons ) );
23985 } else {
23986 $plugin_api = FS_Api::instance(
23987 $user_plugin->id,
23988 'plugin',
23989 $user_plugin->id,
23990 $user_plugin->public_key,
23991 ! $user_plugin->is_live,
23992 false,
23993 $this->get_sdk_version()
23994 );
23995
23996 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
23997
23998 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
23999 is_array( $addons_result->plugins ) &&
24000 ! empty( $addons_result->plugins )
24001 ) {
24002 $has_addons = true;
24003 }
24004 }
24005 }
24006
24007 $user_plugin->has_addons = $has_addons;
24008 }
24009
24010 $is_single_parent_product = ( 1 === count( $user_plugins ) );
24011
24012 $multiple_products_text = '';
24013
24014 if ( $is_single_parent_product ) {
24015 $single_parent_product = reset( $user_plugins );
24016
24017 $thank_you = sprintf(
24018 "<span data-plugin-id='%d'>%s</span>",
24019 $single_parent_product->id,
24020 sprintf(
24021 $single_parent_product->has_addons ?
24022 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
24023 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
24024 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
24025 )
24026 );
24027
24028 $already_opted_in = sprintf(
24029 $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' ),
24030 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
24031 );
24032 } else {
24033 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
24034 $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' );
24035
24036 $products_and_add_ons = '';
24037 foreach ( $user_plugins as $user_plugin ) {
24038 if ( ! empty( $products_and_add_ons ) ) {
24039 $products_and_add_ons .= ', ';
24040 }
24041
24042 if ( ! $user_plugin->has_addons ) {
24043 $products_and_add_ons .= sprintf(
24044 "<span data-plugin-id='%d'>%s</span>",
24045 $user_plugin->id,
24046 $user_plugin->title
24047 );
24048 } else {
24049 $products_and_add_ons .= sprintf(
24050 "<span data-plugin-id='%d'>%s</span>",
24051 $user_plugin->id,
24052 sprintf(
24053 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
24054 $user_plugin->title
24055 )
24056 );
24057 }
24058 }
24059
24060 $multiple_products_text = sprintf(
24061 "<small class='products'><strong>%s:</strong> %s</small>",
24062 $this->get_text_inline( 'Products', 'products' ),
24063 $products_and_add_ons
24064 );
24065 }
24066
24067 $actions = sprintf(
24068 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
24069 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
24070 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
24071 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
24072 sprintf(
24073 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
24074 '<span class="underlined">',
24075 '</span>'
24076 )
24077 );
24078
24079 return sprintf(
24080 '%s %s %s',
24081 $thank_you,
24082 $already_opted_in,
24083 sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '<a href="https://eugdpr.org/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
24084 '<br><br>' .
24085 '<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>' .
24086 $actions .
24087 ( $is_single_parent_product ? '' : $multiple_products_text )
24088 );
24089 }
24090
24091 /**
24092 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
24093 * plugins and themes they've opted in to.
24094 *
24095 * @author Leo Fajardo (@leorw)
24096 * @since 2.1.0
24097 *
24098 * @param string $user_email
24099 * @param string $license_key
24100 * @param array $plugin_ids
24101 * @param string|null $license_key
24102 *
24103 * @return array|false
24104 */
24105 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
24106 $request = array(
24107 'method' => 'POST',
24108 'body' => array(),
24109 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
24110 );
24111
24112 if ( is_string( $user_email ) ) {
24113 $request['body']['email'] = $user_email;
24114 } else {
24115 $request['body']['license_key'] = $license_key;
24116 }
24117
24118 $result = array();
24119
24120 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
24121 $total_plugin_ids = count( $plugin_ids );
24122
24123 $plugin_ids_count_per_request = 10;
24124 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
24125 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
24126
24127 $request['body']['plugin_ids'] = $plugin_ids_set;
24128
24129 $response = self::safe_remote_post(
24130 $url,
24131 $request,
24132 WP_FS__TIME_24_HOURS_IN_SEC,
24133 WP_FS__TIME_12_HOURS_IN_SEC
24134 );
24135
24136 if ( ! is_wp_error( $response ) ) {
24137 $decoded = is_string( $response['body'] ) ?
24138 json_decode( $response['body'] ) :
24139 null;
24140
24141 if (
24142 !is_object($decoded) ||
24143 !isset($decoded->success) ||
24144 true !== $decoded->success ||
24145 !isset( $decoded->data ) ||
24146 !is_array( $decoded->data )
24147 ) {
24148 return false;
24149 }
24150
24151 $result = array_merge( $result, $decoded->data );
24152 }
24153 }
24154
24155 return $result;
24156 }
24157
24158 /**
24159 * @author Leo Fajardo (@leorw)
24160 * @since 2.1.0
24161 */
24162 function _maybe_show_gdpr_admin_notice() {
24163 if ( ! $this->is_user_in_admin() ) {
24164 return;
24165 }
24166
24167 if ( ! $this->should_handle_gdpr_admin_notice() ) {
24168 return;
24169 }
24170
24171 if ( ! $this->is_user_admin() ) {
24172 return;
24173 }
24174
24175 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
24176
24177 $lock = FS_User_Lock::instance();
24178
24179 /**
24180 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
24181 */
24182 if ( ! $lock->try_lock( 60 ) ) {
24183 return;
24184 }
24185
24186 /**
24187 * @var $current_wp_user WP_User
24188 */
24189 $current_wp_user = self::_get_current_wp_user();
24190
24191 /**
24192 * @var FS_User $current_fs_user
24193 */
24194 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
24195
24196 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
24197
24198 if ( ! is_object( $current_fs_user ) ) {
24199 // 10-year lock.
24200 $lock->lock( $ten_years_in_sec );
24201
24202 return;
24203 }
24204
24205 $gdpr = FS_GDPR_Manager::instance();
24206
24207 if ( $gdpr->is_opt_in_notice_shown() ) {
24208 // 30-day lock.
24209 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
24210
24211 return;
24212 }
24213
24214 if ( ! $gdpr->should_show_opt_in_notice() ) {
24215 // 10-year lock.
24216 $lock->lock( $ten_years_in_sec );
24217
24218 return;
24219 }
24220
24221 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
24222 $was_notice_shown_before = ( false !== $last_time_notice_shown );
24223
24224 if ( $was_notice_shown_before &&
24225 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
24226 ) {
24227 // If the notice was shown before, show it again after 30 days from the last time it was shown.
24228 return;
24229 }
24230
24231 /**
24232 * Find all plugin IDs that were installed by the current admin.
24233 */
24234 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
24235
24236 if ( empty( $plugin_ids_map )) {
24237 $lock->lock( $ten_years_in_sec );
24238
24239 return;
24240 }
24241
24242 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
24243 $current_fs_user->email,
24244 null,
24245 array_keys( $plugin_ids_map )
24246 );
24247
24248 if ( empty( $user_plugins ) ) {
24249 $lock->lock(
24250 is_array($user_plugins) ?
24251 $ten_years_in_sec :
24252 // Lock for 24-hours on errors.
24253 WP_FS__TIME_24_HOURS_IN_SEC
24254 );
24255
24256 return;
24257 }
24258
24259 $has_unset_marketing_optin = false;
24260
24261 foreach ( $user_plugins as $user_plugin ) {
24262 if ( true == $user_plugin->is_marketing_allowed ) {
24263 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
24264 }
24265
24266 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
24267 $has_unset_marketing_optin = true;
24268 }
24269 }
24270
24271 if ( empty( $plugin_ids_map ) ||
24272 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
24273 ) {
24274 $lock->lock( $ten_years_in_sec );
24275
24276 return;
24277 }
24278
24279 $modules = array_merge(
24280 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
24281 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
24282 );
24283
24284 foreach ( $modules as $module ) {
24285 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
24286 $plugin_ids_map[ $module->id ] = $module;
24287 }
24288 }
24289
24290 $plugin_title = null;
24291 if ( 1 === count( $plugin_ids_map ) ) {
24292 $module = reset( $plugin_ids_map );
24293 $plugin_title = $module->title;
24294 }
24295
24296 $gdpr->add_opt_in_sticky_notice(
24297 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
24298 $plugin_title
24299 );
24300
24301 $this->add_gdpr_optin_ajax_handler_and_style();
24302
24303 $gdpr->notice_was_just_shown();
24304
24305 // 30-day lock.
24306 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
24307 }
24308
24309 /**
24310 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
24311 * marketing.
24312 *
24313 * @author Leo Fajardo (@leorw)
24314 * @since 2.1.0
24315 */
24316 private function disable_opt_in_notice_and_lock_user() {
24317 FS_GDPR_Manager::instance()->disable_opt_in_notice();
24318
24319 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
24320
24321 // 10-year lock.
24322 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
24323 }
24324
24325 /**
24326 * @author Leo Fajardo (@leorw)
24327 * @since 2.1.0
24328 */
24329 function _add_gdpr_optin_js() {
24330 $vars = array( 'id' => $this->_module_id );
24331
24332 fs_require_once_template( 'gdpr-optin-js.php', $vars );
24333 }
24334
24335 /**
24336 * @author Leo Fajardo (@leorw)
24337 * @since 2.1.0
24338 */
24339 function enqueue_gdpr_optin_notice_style() {
24340 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
24341 }
24342
24343 /**
24344 * @author Leo Fajardo (@leorw)
24345 * @since 2.1.0
24346 */
24347 function _maybe_add_gdpr_optin_ajax_handler() {
24348 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
24349
24350 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
24351 $this->add_gdpr_optin_ajax_handler_and_style();
24352 }
24353 }
24354
24355 /**
24356 * @author Leo Fajardo (@leorw)
24357 * @since 2.1.0
24358 */
24359 function _fetch_is_marketing_required_flag_value_ajax_action() {
24360 $this->_logger->entrance();
24361
24362 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
24363
24364 $license_key = fs_request_get( 'license_key' );
24365
24366 if ( empty($license_key) ) {
24367 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
24368 }
24369
24370 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
24371 null,
24372 $license_key,
24373 array( $this->_module_id )
24374 );
24375
24376 if ( ! is_array( $user_plugins ) ||
24377 empty($user_plugins) ||
24378 !isset($user_plugins[0]->plugin_id) ||
24379 $user_plugins[0]->plugin_id != $this->_module_id
24380 ) {
24381 /**
24382 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
24383 *
24384 * @author Vova Feldman (@svovaf)
24385 */
24386 self::shoot_ajax_success( array(
24387 'is_marketing_allowed' => null,
24388 'license_owner_id' => null
24389 ) );
24390 }
24391
24392 self::shoot_ajax_success( array(
24393 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
24394 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
24395 ) );
24396 }
24397
24398 /**
24399 * @author Leo Fajardo (@leorw)
24400 * @since 2.3.2
24401 *
24402 * @param number[] $install_ids
24403 *
24404 * @return array {
24405 * An array of objects containing the installs' licenses owners data.
24406 *
24407 * @property number $id User ID.
24408 * @property string $email User email (can be masked email).
24409 * }
24410 */
24411 private function fetch_installs_licenses_owners_data( $install_ids ) {
24412 $this->_logger->entrance();
24413
24414 $response = $this->get_api_user_scope()->get(
24415 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
24416 );
24417
24418 $license_owners = null;
24419
24420 if ( $this->is_api_result_object( $response, 'owners' ) ) {
24421 $license_owners = $response->owners;
24422 }
24423
24424 return $license_owners;
24425 }
24426
24427 /**
24428 * @author Leo Fajardo (@leorw)
24429 * @since 2.1.0
24430 */
24431 private function add_gdpr_optin_ajax_handler_and_style() {
24432 // Add GDPR action AJAX callback.
24433 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
24434
24435 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
24436 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
24437 }
24438
24439 /**
24440 * @author Leo Fajardo (@leorw)
24441 * @since 2.1.0
24442 */
24443 function _gdpr_optin_ajax_action() {
24444 $this->_logger->entrance();
24445
24446 $this->check_ajax_referer( 'gdpr_optin_action' );
24447
24448 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
24449 self::shoot_ajax_failure();
24450 }
24451
24452 $current_wp_user = self::_get_current_wp_user();
24453
24454 $plugin_ids = fs_request_get( 'plugin_ids', array() );
24455 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
24456 self::shoot_ajax_failure();
24457 }
24458
24459 $modules = array_merge(
24460 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
24461 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
24462 );
24463
24464 foreach ( $modules as $key => $module ) {
24465 if ( ! in_array( $module->id, $plugin_ids ) ) {
24466 unset( $modules[ $key ] );
24467 }
24468 }
24469
24470 if ( empty( $modules ) ) {
24471 self::shoot_ajax_failure();
24472 }
24473
24474 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
24475
24476 foreach ( $modules as $module ) {
24477 $user_api->call( "?plugin_id={$module->id}", 'put', array(
24478 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
24479 ) );
24480 }
24481
24482 FS_GDPR_Manager::instance()->remove_opt_in_notice();
24483
24484 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
24485
24486 // 10-year lock.
24487 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
24488
24489 self::shoot_ajax_success();
24490 }
24491
24492 /**
24493 * 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.
24494 *
24495 * @author Vova Feldman (@svovaf)
24496 * @since 2.1.0
24497 *
24498 * @return bool
24499 */
24500 private function should_handle_gdpr_admin_notice() {
24501 return $this->apply_filters(
24502 'handle_gdpr_admin_notice',
24503 // Default to false.
24504 false
24505 );
24506 }
24507
24508 #endregion
24509
24510 #----------------------------------------------------------------------------------
24511 #region Marketing
24512 #----------------------------------------------------------------------------------
24513
24514 /**
24515 * Check if current user purchased any other plugins before.
24516 *
24517 * @author Vova Feldman (@svovaf)
24518 * @since 1.0.9
24519 *
24520 * @return bool
24521 */
24522 function has_purchased_before() {
24523 // TODO: Implement has_purchased_before() method.
24524 throw new Exception( 'not implemented' );
24525 }
24526
24527 /**
24528 * Check if current user classified as an agency.
24529 *
24530 * @author Vova Feldman (@svovaf)
24531 * @since 1.0.9
24532 *
24533 * @return bool
24534 */
24535 function is_agency() {
24536 // TODO: Implement is_agency() method.
24537 throw new Exception( 'not implemented' );
24538 }
24539
24540 /**
24541 * Check if current user classified as a developer.
24542 *
24543 * @author Vova Feldman (@svovaf)
24544 * @since 1.0.9
24545 *
24546 * @return bool
24547 */
24548 function is_developer() {
24549 // TODO: Implement is_developer() method.
24550 throw new Exception( 'not implemented' );
24551 }
24552
24553 /**
24554 * Check if current user classified as a business.
24555 *
24556 * @author Vova Feldman (@svovaf)
24557 * @since 1.0.9
24558 *
24559 * @return bool
24560 */
24561 function is_business() {
24562 // TODO: Implement is_business() method.
24563 throw new Exception( 'not implemented' );
24564 }
24565
24566 #endregion
24567
24568 #----------------------------------------------------------------------------------
24569 #region Helper
24570 #----------------------------------------------------------------------------------
24571
24572 /**
24573 * If running with a secret key, assume it's the developer and show pending plans as well.
24574 *
24575 * @author Vova Feldman (@svovaf)
24576 * @since 2.1.2
24577 *
24578 * @param string $path
24579 *
24580 * @return string
24581 */
24582 function add_show_pending( $path ) {
24583 if ( ! $this->has_secret_key() ) {
24584 return $path;
24585 }
24586
24587 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
24588 }
24589
24590 #endregion
24591 }
24592