PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.37
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.37
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / freemius / includes / class-freemius.php

class-freemius.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.37, at freemius/includes/class-freemius.php

26,359 lines 992.3 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 2.9.1
114 * @var string|null Hints the SDK whether the plugin supports parallel activation mode, preventing the auto-deactivation of the free version when the premium version is activated, and vice versa.
115 */
116 private $_premium_plugin_basename_from_parallel_activation;
117
118 /**
119 * @since 1.1.7.5
120 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
121 */
122 private $_anonymous_mode;
123
124 /**
125 * @since 1.1.9
126 * @var bool Hints the SDK if plugin have any free plans.
127 */
128 private $_is_premium_only;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin have premium code version at all.
133 */
134 private $_has_premium_version;
135
136 /**
137 * @since 1.2.1.6
138 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
139 */
140 private $_ignore_pending_mode;
141
142 /**
143 * @since 1.0.8
144 * @var bool Hints the SDK if the plugin has any paid plans.
145 */
146 private $_has_paid_plans;
147
148 /**
149 * @since 1.2.1.5
150 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
151 * without a specified period, if positive - the number of trial days.
152 */
153 private $_trial_days = - 1;
154
155 /**
156 * @since 1.2.1.5
157 * @var bool Hints the SDK if the trial requires a payment method or not.
158 */
159 private $_is_trial_require_payment = false;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
164 */
165 private $_is_org_compliant;
166
167 /**
168 * @since 1.0.7
169 * @var bool Hints the SDK if the plugin is has add-ons.
170 */
171 private $_has_addons;
172
173 /**
174 * @since 2.4.5
175 * @var string Navigation type: 'menu' or 'tabs'.
176 */
177 private $_navigation;
178
179 const NAVIGATION_MENU = 'menu';
180 const NAVIGATION_TABS = 'tabs';
181
182 /**
183 * @since 1.1.6
184 * @var string[]bool.
185 */
186 private $_permissions;
187
188 /**
189 * @var FS_Storage
190 */
191 private $_storage;
192
193 /**
194 * @since 1.2.2.7
195 * @var FS_Cache_Manager
196 */
197 private $_cache;
198
199 /**
200 * @since 1.0.0
201 *
202 * @var FS_Logger
203 */
204 private $_logger;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin
209 */
210 private $_plugin = false;
211 /**
212 * @since 1.0.4
213 *
214 * @var FS_Plugin|false
215 */
216 private $_parent_plugin = false;
217 /**
218 * @since 1.1.1
219 *
220 * @var Freemius
221 */
222 private $_parent = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_User
227 */
228 private $_user = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Site
233 */
234 private $_site = false;
235 /**
236 * @since 1.0.1
237 *
238 * @var FS_Plugin_License
239 */
240 private $_license;
241 /**
242 * @since 1.0.2
243 *
244 * @var FS_Plugin_Plan[]
245 */
246 private $_plans = false;
247 /**
248 * @var FS_Plugin_License[]
249 * @since 1.0.5
250 */
251 private $_licenses = false;
252
253 /**
254 * @since 1.0.1
255 *
256 * @var FS_Admin_Menu_Manager
257 */
258 private $_menu;
259
260 /**
261 * @var FS_Admin_Notices
262 */
263 private $_admin_notices;
264
265 /**
266 * @since 1.1.6
267 *
268 * @var FS_Admin_Notices
269 */
270 private static $_global_admin_notices;
271
272 /**
273 * @var FS_Logger
274 * @since 1.0.0
275 */
276 private static $_static_logger;
277
278 /**
279 * @var FS_Options
280 * @since 1.0.2
281 */
282 private static $_accounts;
283
284 /**
285 * @since 1.2.2
286 *
287 * @var number
288 */
289 private $_module_id;
290
291 /**
292 * @var Freemius[]
293 */
294 private static $_instances = array();
295
296 /**
297 * @since 1.2.3
298 *
299 * @var FS_Affiliate
300 */
301 private $affiliate = null;
302
303 /**
304 * @since 1.2.3
305 *
306 * @var FS_AffiliateTerms
307 */
308 private $plugin_affiliate_terms = null;
309
310 /**
311 * @since 1.2.3
312 *
313 * @var FS_AffiliateTerms
314 */
315 private $custom_affiliate_terms = null;
316
317 /**
318 * @since 2.0.0
319 *
320 * @var bool
321 */
322 private $_is_multisite_integrated;
323
324 /**
325 * @since 2.0.0
326 *
327 * @var bool True if the current request is for a network admin screen and the plugin is network active.
328 */
329 private $_is_network_active;
330
331 /**
332 * @since 2.0.0
333 *
334 * @var int|null The original blog ID the plugin was loaded with.
335 */
336 private $_blog_id = null;
337
338 /**
339 * @since 2.0.0
340 *
341 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
342 */
343 private $_context_is_network_or_blog_id = null;
344
345 /**
346 * @since 2.0.0
347 *
348 * @var string
349 */
350 private $_dynamically_added_top_level_page_hook_name = '';
351
352 /**
353 * @author Leo Fajardo (@leorw)
354 * @since 2.3.1
355 *
356 * @var bool
357 */
358 private $is_whitelabeled;
359
360 /**
361 * @author Leo Fajardo (@leorw)
362 * @since 2.4.0
363 *
364 * @var bool
365 */
366 private $_is_bundle_license_auto_activation_enabled = false;
367
368 #region Uninstall Reasons IDs
369
370 const REASON_NO_LONGER_NEEDED = 1;
371 const REASON_FOUND_A_BETTER_PLUGIN = 2;
372 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
373 const REASON_BROKE_MY_SITE = 4;
374 const REASON_SUDDENLY_STOPPED_WORKING = 5;
375 const REASON_CANT_PAY_ANYMORE = 6;
376 const REASON_OTHER = 7;
377 const REASON_DIDNT_WORK = 8;
378 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
379 const REASON_COULDNT_MAKE_IT_WORK = 10;
380 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
381 const REASON_NOT_WORKING = 12;
382 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
383 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
384 const REASON_TEMPORARY_DEACTIVATION = 15;
385
386 #endregion
387
388 /**
389 * @author Leo Fajardo (@leorw)
390 * @since 2.4.2
391 *
392 * @var string|null
393 */
394 private $_pricing_js_path = null;
395
396 const VERSION_MAX_CHARS = 16;
397 const LANGUAGE_MAX_CHARS = 8;
398
399 /* Ctor
400 ------------------------------------------------------------------------------------------------------------------*/
401
402 /**
403 * Main singleton instance.
404 *
405 * @author Vova Feldman (@svovaf)
406 * @since 1.0.0
407 *
408 * @param number $module_id
409 * @param string|bool $slug
410 * @param bool $is_init Since 1.2.1 Is initiation sequence.
411 */
412 private function __construct( $module_id, $slug = false, $is_init = false ) {
413 $main_file = false;
414
415 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
416 $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
417 }
418
419 $this->_module_id = $module_id;
420 $this->_slug = $this->get_slug();
421 $this->_module_type = $this->get_module_type();
422
423 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
424
425 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
426
427 // If not set or 24 hours have already passed from the last time it's set, set the last load timestamp to the current time.
428 if (
429 ! isset( $this->_storage->last_load_timestamp ) ||
430 $this->_storage->last_load_timestamp < ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC ) )
431 ) {
432 $this->_storage->last_load_timestamp = time();
433 }
434
435 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
436
437 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
438
439 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
440 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
441 $this->_plugin_basename = $this->get_plugin_basename();
442 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
443
444 $this->_is_multisite_integrated = (
445 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
446 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
447 );
448
449 $this->_is_network_active = (
450 is_multisite() &&
451 $this->_is_multisite_integrated &&
452 // Themes are always network activated, but the ACTUAL activation is per site.
453 $this->is_plugin() &&
454 (
455 is_plugin_active_for_network( $this->_plugin_basename ) ||
456 // Plugin network level activation or uninstall.
457 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
458 )
459 );
460
461 $this->_storage->set_network_active(
462 $this->_is_network_active,
463 $this->is_delegated_connection()
464 );
465
466 if ( ! isset( $this->_storage->is_network_activated ) ) {
467 $this->_storage->is_network_activated = $this->_is_network_active;
468 }
469
470 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
471 // Update last activation level.
472 $this->_storage->is_network_activated = $this->_is_network_active;
473
474 $this->maybe_adjust_storage();
475 }
476
477 #region Migration
478
479 if ( is_multisite() ) {
480 /**
481 * If the install_timestamp exists on the site level but doesn't exist on the
482 * network level storage, it means that we need to process the storage with migration.
483 *
484 * 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.
485 *
486 * @author Vova Feldman (@svovaf)
487 * @since 2.0.0
488 */
489 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
490 false !== $this->_storage->get( 'install_timestamp', false, false )
491 ) {
492 // Initiate storage migration.
493 $this->_storage->migrate_to_network();
494
495 // Migrate module cache to network level storage.
496 $this->_cache->migrate_to_network();
497 }
498 }
499
500 #endregion
501
502 $base_name_split = explode( '/', $this->_plugin_basename );
503 $this->_plugin_dir_name = $base_name_split[0];
504
505 if ( $this->_logger->is_on() ) {
506 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
507 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
508 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
509 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
510 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
511 }
512
513 // Remember link between file to slug.
514 $this->store_file_slug_map();
515
516 // Store plugin's initial install timestamp.
517 if ( ! isset( $this->_storage->install_timestamp ) ) {
518 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
519 }
520
521 if ( ! is_object( $this->_plugin ) ) {
522 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
523 }
524
525 $this->_admin_notices = FS_Admin_Notices::instance(
526 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
527 /**
528 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
529 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
530 *
531 * @author Leo Fajardo (@leorw)
532 * @since 1.2.2
533 */
534 ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
535 $this->_plugin->title :
536 $this->get_plugin_name()
537 ),
538 $this->get_unique_affix()
539 );
540
541 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
542 fs_request_is_action( 'restart_freemius' )
543 ) {
544 FS_Api::clear_cache();
545 $this->_cache->clear();
546 }
547
548 $this->register_constructor_hooks();
549
550 /**
551 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
552 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
553 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
554 *
555 * @author Leo Fajardo (@leorw)
556 */
557 self::migrate_install_plan_to_plan_id( $this->_storage );
558
559 $this->_load_account();
560
561 $this->_version_updates_handler();
562 }
563
564 /**
565 * @author Leo Fajardo (@leorw)
566 * @since 2.3.0
567 */
568 private function maybe_adjust_storage() {
569 $install_timestamp = null;
570 $prev_is_premium = null;
571
572 $options_to_update = array();
573
574 $is_network_admin = fs_is_network_admin();
575
576 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
577
578 if ( ! $is_network_admin ) {
579 if ( is_null( $network_install_timestamp ) ) {
580 // Plugin was not network-activated before.
581 return;
582 }
583
584 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
585 // Set the `install_timestamp` only if it's not yet set.
586 $install_timestamp = $network_install_timestamp;
587 }
588
589 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
590 } else {
591 $current_wp_user = self::_get_current_wp_user();
592 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
593 $network_user_info = array();
594
595 $skips_count = 0;
596
597 $sites = self::get_sites();
598 $sites_count = count( $sites );
599
600 $blog_id_2_install_map = array();
601
602 $is_first_non_ignored_blog = true;
603
604 foreach ( $sites as $site ) {
605 $blog_id = self::get_site_blog_id( $site );
606
607 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
608
609 if ( is_null( $blog_install_timestamp ) ) {
610 // Plugin has not been installed on this blog.
611 continue;
612 }
613
614 $is_earlier_install = (
615 ! is_null( $install_timestamp ) &&
616 $blog_install_timestamp < $install_timestamp
617 );
618
619 $install = $this->get_install_by_blog_id( $blog_id );
620
621 $update_network_user_info = false;
622
623 if ( ! is_object( $install ) ) {
624 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
625 // The opt-in decision (whether to skip or opt in) is yet to be made.
626 continue;
627 }
628
629 $skips_count ++;
630 } else {
631 $blog_id_2_install_map[ $blog_id ] = $install;
632
633 if ( empty( $network_user_info ) ) {
634 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
635 $update_network_user_info = true;
636 }
637
638 if ( ! $update_network_user_info &&
639 is_object( $current_fs_user ) &&
640 $network_user_info['user_id'] != $current_fs_user->id &&
641 $install->user_id == $current_fs_user->id
642 ) {
643 // If an install that is owned by the current WP user is found, use its user information instead.
644 $update_network_user_info = true;
645 }
646
647 if ( ! $update_network_user_info &&
648 $is_earlier_install &&
649 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
650 ) {
651 // 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.
652 $update_network_user_info = true;
653 }
654 }
655
656 if ( $update_network_user_info ) {
657 $network_user_info = array(
658 'user_id' => $install->user_id,
659 'blog_id' => $blog_id
660 );
661 }
662
663 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
664
665 if ( $is_first_non_ignored_blog ) {
666 $prev_is_premium = $site_prev_is_premium;
667
668 if ( is_null( $network_install_timestamp ) ) {
669 $install_timestamp = $blog_install_timestamp;
670 }
671
672 $is_first_non_ignored_blog = false;
673
674 continue;
675 }
676
677 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
678 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
679 $prev_is_premium = null;
680 }
681
682 if ( $is_earlier_install ) {
683 // If an earlier install timestamp is found.
684 $install_timestamp = $blog_install_timestamp;
685 }
686 }
687
688 $installs_count = count( $blog_id_2_install_map );
689
690 if ( $sites_count === ( $installs_count + $skips_count ) ) {
691 if ( ! empty( $network_user_info ) ) {
692 $options_to_update['network_user_id'] = $network_user_info['user_id'];
693 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
694
695 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
696 if ( $install->user_id == $network_user_info['user_id'] ) {
697 continue;
698 }
699
700 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
701 }
702 }
703
704 if ( $sites_count === $skips_count ) {
705 /**
706 * Assume network-level skipping as the intended action if all actions identified were only
707 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
708 */
709 $options_to_update['is_anonymous_ms'] = true;
710 } else if ( $sites_count === $installs_count ) {
711 /**
712 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
713 * (i.e., no delegation and skipping of the connections so far).
714 */
715 $options_to_update['is_network_connected'] = true;
716 }
717 }
718 }
719
720 if ( ! is_null( $install_timestamp ) ) {
721 $options_to_update['install_timestamp'] = $install_timestamp;
722 }
723
724 if ( ! is_null( $prev_is_premium ) ) {
725 $options_to_update['prev_is_premium'] = $prev_is_premium;
726 }
727
728 if ( ! empty( $options_to_update ) ) {
729 $this->adjust_storage( $options_to_update, $is_network_admin );
730 }
731 }
732
733 /**
734 * @author Leo Fajardo (@leorw)
735 * @since 2.3.0
736 *
737 * @param array $options
738 * @param bool $is_network_admin
739 */
740 private function adjust_storage( $options, $is_network_admin ) {
741 foreach ( $options as $name => $value ) {
742 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
743 }
744 }
745
746 /**
747 * Checks whether this module has a settings menu.
748 *
749 * @author Leo Fajardo (@leorw)
750 * @since 1.2.2
751 *
752 * @return bool
753 */
754 function has_settings_menu() {
755 return ( $this->_is_network_active && fs_is_network_admin() ) ?
756 $this->_menu->has_network_menu() :
757 $this->_menu->has_menu();
758 }
759
760 /**
761 * 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.
762 *
763 * @author Vova Feldman (@svovaf)
764 * @since 2.4.5
765 *
766 * @return bool
767 */
768 function show_opt_in_on_themes_page() {
769 if ( ! $this->is_free_wp_org_theme() ) {
770 return false;
771 }
772
773 if ( ! $this->has_settings_menu() ) {
774 return true;
775 }
776
777 return $this->show_settings_with_tabs();
778 }
779
780 /**
781 * If `true` the opt-in should be shown on the product's main setting page.
782 *
783 * @author Vova Feldman (@svovaf)
784 * @since 2.4.5
785 *
786 * @return bool
787 *
788 * @uses show_opt_in_on_themes_page();
789 */
790 function show_opt_in_on_setting_page() {
791 return ! $this->show_opt_in_on_themes_page();
792 }
793
794 /**
795 * If `true` the settings should be shown using tabs.
796 *
797 * @author Vova Feldman (@svovaf)
798 * @since 2.4.5
799 *
800 * @return bool
801 */
802 function show_settings_with_tabs() {
803 return ( self::NAVIGATION_TABS === $this->_navigation );
804 }
805
806 /**
807 * Check if the context module is free wp.org theme.
808 *
809 * This method is helpful because:
810 * 1. wp.org themes are limited to a single submenu item,
811 * and sub-submenu items are most likely not allowed (never verified).
812 * 2. wp.org themes are not allowed to redirect the user
813 * after the theme activation, therefore, the agreed UX
814 * is showing the opt-in as a modal dialog box after
815 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
816 *
817 * @author Vova Feldman (@svovaf)
818 * @since 1.2.2.7
819 *
820 * @return bool
821 */
822 function is_free_wp_org_theme() {
823 return (
824 $this->is_theme() &&
825 $this->is_org_repo_compliant() &&
826 ! $this->is_premium()
827 );
828 }
829
830 /**
831 * Checks whether this a submenu item is visible.
832 *
833 * @author Vova Feldman (@svovaf)
834 * @since 1.2.2.6
835 * @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.
836 *
837 * @param string $slug
838 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
839 *
840 * @return bool
841 */
842 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
843 if ( $this->is_admin_page( $slug ) ) {
844 /**
845 * It is the current context page, so show the submenu item
846 * so the user will have the right context page, even if it
847 * was set to hidden.
848 */
849 return true;
850 }
851
852 if ( ! $this->has_settings_menu() ) {
853 // No menu settings at all.
854 return false;
855 }
856
857 if (
858 ! $is_tabs_visibility_check &&
859 $this->is_org_repo_compliant() &&
860 $this->show_settings_with_tabs()
861 ) {
862 /**
863 * wp.org themes are limited to a single submenu item, and
864 * sub-submenu items are most likely not allowed (never verified).
865 */
866 return false;
867 }
868
869 return $this->_menu->is_submenu_item_visible( $slug );
870 }
871
872 /**
873 * Check if a Freemius page should be accessible via the UI.
874 *
875 * @author Vova Feldman (@svovaf)
876 * @since 1.2.2.7
877 *
878 * @param string $slug
879 *
880 * @return bool
881 */
882 function is_page_visible( $slug ) {
883 if ( $this->is_admin_page( $slug ) ) {
884 return true;
885 }
886
887 return $this->_menu->is_submenu_item_visible( $slug, true, true );
888 }
889
890 /**
891 * @author Vova Feldman (@svovaf)
892 * @since 1.0.9
893 */
894 private function _version_updates_handler() {
895 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
896 // Freemius version upgrade mode.
897 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
898 $this->_storage->sdk_version = $this->version;
899
900 if ( empty( $this->_storage->sdk_last_version ) ||
901 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
902 ) {
903 $this->_storage->sdk_upgrade_mode = true;
904 $this->_storage->sdk_downgrade_mode = false;
905 } else {
906 $this->_storage->sdk_downgrade_mode = true;
907 $this->_storage->sdk_upgrade_mode = false;
908
909 }
910
911 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
912 }
913
914 $plugin_version = $this->get_plugin_version();
915 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
916 // Plugin version upgrade mode.
917 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
918 $this->_storage->plugin_version = $plugin_version;
919
920 if ( empty( $this->_storage->plugin_last_version ) ||
921 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
922 ) {
923 $this->_storage->plugin_upgrade_mode = true;
924 $this->_storage->plugin_downgrade_mode = false;
925 } else {
926 $this->_storage->plugin_downgrade_mode = true;
927 $this->_storage->plugin_upgrade_mode = false;
928 }
929
930 if ( ! empty( $this->_storage->plugin_last_version ) ) {
931 // Different version of the plugin was installed before, therefore it's an update.
932 $this->_storage->is_plugin_new_install = false;
933 }
934
935 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
936 }
937 }
938
939 #--------------------------------------------------------------------------------
940 #region Data Migration on SDK Update
941 #--------------------------------------------------------------------------------
942
943 /**
944 * @author Vova Feldman (@svovaf)
945 * @since 1.1.5
946 *
947 * @param string $sdk_prev_version
948 * @param string $sdk_version
949 */
950 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
951 if ( empty( $sdk_prev_version ) ) {
952 return;
953 }
954
955 if (
956 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
957 version_compare( $sdk_version, '2.5.1', '>=' )
958 ) {
959 if ( $this->is_registered( true ) ) {
960 /**
961 * Migrate to new permissions layer.
962 */
963 require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
964
965 $install_by_blog_id = is_multisite() ?
966 $this->get_blog_install_map() :
967 array( 0 => $this->_site );
968
969 fs_migrate_251( $this, $install_by_blog_id );
970 }
971 }
972 }
973
974 /**
975 * @author Leo Fajardo (@leorw)
976 * @since 2.0.0
977 *
978 * @param \FS_Storage $storage
979 * @param bool|int|null $blog_id
980 */
981 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
982 if ( empty( $storage->sdk_version ) ) {
983 // New installation of the plugin, no need to upgrade.
984 return;
985 }
986
987 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
988 // Previous version is >= 2.0.0, so no need to migrate.
989 return;
990 }
991
992 // Alias.
993 $module_type = $storage->get_module_type();
994 $module_slug = $storage->get_module_slug();
995
996 $installs = self::get_all_sites( $module_type, $blog_id );
997 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
998
999 if ( ! is_object( $install ) ) {
1000 return;
1001 }
1002
1003 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
1004 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
1005 $install->plan_id = self::_decrypt( $install->plan->id );
1006 }
1007
1008 unset( $install->plan );
1009
1010 $installs[ $module_slug ] = clone $install;
1011
1012 self::set_account_option_by_module(
1013 $module_type,
1014 'sites',
1015 $installs,
1016 true,
1017 $blog_id
1018 );
1019 }
1020 }
1021
1022 /**
1023 * @author Vova Feldman (@svovaf)
1024 * @since 1.2.2.7
1025 *
1026 * @param string $plugin_prev_version
1027 * @param string $plugin_version
1028 */
1029 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1030 if ( $this->is_theme() ) {
1031 // Expire the cache of the previous tabs since the theme may
1032 // have setting updates.
1033 $this->_cache->expire( 'tabs' );
1034 $this->_cache->expire( 'tabs_stylesheets' );
1035 }
1036 }
1037
1038 /**
1039 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1040 * - Moves some data to the network level storage.
1041 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1042 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1043 * - 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.
1044 * - 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.
1045 * - 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.
1046 *
1047 * @author Vova Feldman (@svovaf)
1048 * @since 2.0.0
1049 */
1050 private static function migrate_accounts_to_network() {
1051 $sites = self::get_sites();
1052 $sites_count = count( $sites );
1053 $connection_status = array();
1054 $plugin_slugs = array();
1055 foreach ( $sites as $site ) {
1056 $blog_id = self::get_site_blog_id( $site );
1057
1058 self::$_accounts->migrate_to_network( $blog_id );
1059
1060 /**
1061 * Build a list of all Freemius powered plugins slugs.
1062 */
1063 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1064 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1065 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1066 $plugin_slugs[ $data['slug'] ] = true;
1067 }
1068 }
1069
1070 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1071
1072 if ( is_array( $installs ) ) {
1073 foreach ( $installs as $slug => $install ) {
1074 if ( ! isset( $connection_status[ $slug ] ) ) {
1075 $connection_status[ $slug ] = array();
1076 }
1077
1078 if ( is_object( $install ) &&
1079 FS_Site::is_valid_id( $install->id ) &&
1080 FS_User::is_valid_id( $install->user_id )
1081 ) {
1082 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1083 }
1084 }
1085 }
1086 }
1087
1088 foreach ( $plugin_slugs as $slug => $true ) {
1089 if ( ! isset( $connection_status[ $slug ] ) ) {
1090 $connection_status[ $slug ] = array();
1091 }
1092
1093 foreach ( $sites as $site ) {
1094 $blog_id = self::get_site_blog_id( $site );
1095
1096 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1097 continue;
1098 }
1099
1100 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1101
1102 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1103
1104 if ( ! is_null( $is_anonymous ) ) {
1105 // Since 1.1.3 is_anonymous is an array.
1106 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1107 $is_anonymous = $is_anonymous['is'];
1108 }
1109
1110 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1111 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1112 }
1113 }
1114
1115 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1116 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1117 }
1118 }
1119 }
1120
1121 $super_admins = array();
1122
1123 foreach ( $connection_status as $slug => $blogs_status ) {
1124 $skips = 0;
1125 $ignores = 0;
1126 $connections = 0;
1127 $opted_in_users = array();
1128 $opted_in_super_admins = array();
1129
1130 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1131
1132 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1133 if ( 'skipped' === $status_or_user_id ) {
1134 $skips ++;
1135 } else if ( 'ignored' === $status_or_user_id ) {
1136 $ignores ++;
1137 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1138 $connections ++;
1139
1140 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1141 $opted_in_users[ $status_or_user_id ] = array();
1142 }
1143
1144 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1145
1146 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1147 self::is_super_admin( $status_or_user_id )
1148 ) {
1149 // Cache super-admin data.
1150 $super_admins[ $status_or_user_id ] = true;
1151
1152 // Remember opted-in super-admins for the plugin.
1153 $opted_in_super_admins[ $status_or_user_id ] = true;
1154 }
1155 }
1156 }
1157
1158 $main_super_admin_user_id = null;
1159 $all_migrated = false;
1160 if ( $sites_count == $skips ) {
1161 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1162 $storage->is_anonymous_ms = $storage->is_anonymous;
1163
1164 $all_migrated = true;
1165 } else if ( $sites_count == $ignores ) {
1166 // Don't do anything, still in activation mode.
1167
1168 $all_migrated = true;
1169 } else if ( 0 < count( $opted_in_super_admins ) ) {
1170 // Find the super-admin with the majority of installs.
1171 $max_installs_by_super_admin = 0;
1172 foreach ( $opted_in_super_admins as $user_id => $true ) {
1173 $installs_count = count( $opted_in_users[ $user_id ] );
1174
1175 if ( $installs_count > $max_installs_by_super_admin ) {
1176 $max_installs_by_super_admin = $installs_count;
1177 $main_super_admin_user_id = $user_id;
1178 }
1179 }
1180
1181 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1182 // Super-admin opted-in for all sites in the network.
1183 $storage->is_network_connected = true;
1184
1185 $all_migrated = true;
1186 }
1187
1188 // Store network user.
1189 $storage->network_user_id = $main_super_admin_user_id;
1190
1191 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1192 // Since all sites are opted-in, associating with the main site.
1193 get_current_blog_id() :
1194 // Associating with the 1st found opted-in site.
1195 $opted_in_users[ $main_super_admin_user_id ][0];
1196
1197 /**
1198 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1199 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1200 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1201 * will be empty.
1202 */
1203 $storage->migrate_to_network();
1204 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1205 } else {
1206 // At least one opt-in. All the opt-in were created by a non-super-admin.
1207 if ( 0 == $ignores ) {
1208 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1209 $storage->store( 'is_delegated_connection', true, true );
1210
1211 $all_migrated = true;
1212 }
1213 }
1214
1215 if ( ! $all_migrated ) {
1216 /**
1217 * Delegate all sites that were:
1218 * 1) Opted-in by a user that is NOT the main-super-admin.
1219 * 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.
1220 */
1221 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1222 if ( $status_or_user_id == $main_super_admin_user_id ) {
1223 continue;
1224 }
1225
1226 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1227 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1228 ) {
1229 $storage->store( 'is_delegated_connection', true, $blog_id );
1230 }
1231 }
1232 }
1233
1234
1235 if ( ( $connections + $skips > 0 ) ) {
1236 if ( $ignores > 0 ) {
1237 /**
1238 * If admin already opted-in or skipped in any of the network sites, and also
1239 * have sites which the connection decision was not yet taken, set this plugin
1240 * into network activation mode so the super-admin can choose what to do with
1241 * the rest of the sites.
1242 */
1243 self::set_network_upgrade_mode( $storage );
1244 }
1245 }
1246 }
1247 }
1248
1249 /**
1250 * Set a module into network upgrade mode.
1251 *
1252 * @author Vova Feldman (@svovaf)
1253 * @since 2.0.0
1254 *
1255 * @param \FS_Storage $storage
1256 *
1257 * @return bool
1258 */
1259 public static function set_network_upgrade_mode( FS_Storage $storage ) {
1260 return $storage->is_network_activation = true;
1261 }
1262
1263 /**
1264 * Will return true after upgrading to the SDK with the network level integration,
1265 * when the super-admin involvement is required regarding the rest of the sites.
1266 *
1267 * @author Vova Feldman (@svovaf)
1268 * @since 2.0.0
1269 *
1270 * @return bool
1271 */
1272 function is_network_upgrade_mode() {
1273 return $this->_storage->get( 'is_network_activation' );
1274 }
1275
1276 /**
1277 * Clear flag after the upgrade mode completion.
1278 *
1279 * @author Vova Feldman (@svovaf)
1280 * @since 2.0.0
1281 *
1282 * @return bool True if network activation was on and now completed.
1283 */
1284 private function network_upgrade_mode_completed() {
1285 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1286 $this->_storage->remove( 'is_network_activation' );
1287
1288 return true;
1289 }
1290
1291 return false;
1292 }
1293
1294 #endregion
1295
1296 /**
1297 * This action is connected to the 'plugins_loaded' hook and helps to determine
1298 * if this is a new plugin installation or a plugin update.
1299 *
1300 * There are 3 different use-cases:
1301 * 1) New plugin installation right with Freemius:
1302 * 1.1 _activate_plugin_event_hook() will be executed first
1303 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1304 * and $this->_storage->plugin_last_version is not set,
1305 * $this->_storage->is_plugin_new_install will be set to TRUE.
1306 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1307 * be already set to TRUE.
1308 *
1309 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1310 * 2.1 _activate_plugin_event_hook() will not be executed, because
1311 * the activation hook do NOT fires on updates since WP 3.1.
1312 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1313 * be empty, therefore, it will be set to FALSE.
1314 *
1315 * 3) Plugin update, had Freemius in prev version as well:
1316 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1317 * before, $this->_storage->plugin_last_version will NOT be empty,
1318 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1319 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1320 * already set, therefore, it will not be modified.
1321 *
1322 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1323 *
1324 * NOTE:
1325 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1326 * and then, the next immediate PageView is the plugin's main settings page, it will not
1327 * show the opt-in right away. The reason it will happen is because Freemius execution
1328 * will be turned off till the plugin is fully loaded at least once
1329 * (till $this->_storage->was_plugin_loaded is TRUE).
1330 *
1331 * @author Vova Feldman (@svovaf)
1332 * @since 1.1.9
1333 *
1334 */
1335 function _plugins_loaded() {
1336 // Update flag that plugin was loaded with Freemius at least once.
1337 $this->_storage->was_plugin_loaded = true;
1338
1339 /**
1340 * Bug fix - only set to false when it's a plugin, due to the
1341 * execution sequence of the theme hooks and our methods, if
1342 * this will be set for themes, Freemius will always assume
1343 * it's a theme update.
1344 *
1345 * @author Vova Feldman (@svovaf)
1346 * @since 1.2.2.2
1347 */
1348 if ( $this->is_plugin() &&
1349 ! isset( $this->_storage->is_plugin_new_install )
1350 ) {
1351 $this->_storage->is_plugin_new_install = (
1352 ! is_plugin_active( $this->_plugin_basename ) &&
1353 empty( $this->_storage->plugin_last_version )
1354 );
1355 }
1356 }
1357
1358 function _run_garbage_collector() {
1359 if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', true ) ) {
1360 return;
1361 }
1362
1363 if ( ! $this->is_user_in_admin() ) {
1364 return;
1365 }
1366
1367 require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
1368
1369 $lock = new FS_Lock( 'garbage_collection' );
1370
1371 if ( $lock->is_locked() ) {
1372 return;
1373 }
1374
1375 // Create a 1-day lock.
1376 $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
1377
1378 FS_Garbage_Collector::instance()->clean();
1379 }
1380
1381 /**
1382 * Modifies all external links in the submenu by altering their href, and also opens them in new tab if needed.
1383 *
1384 * @author Vova Feldman (@svovaf)
1385 * @author Swashata Ghosh (@swashata)
1386 * @since 2.1.4
1387 */
1388 static function _handle_submenu_external_link() {
1389 ?>
1390 <script type="text/javascript">
1391 (function ( $ ) {
1392 $( '.fs-submenu-item' ).each( function () {
1393 var $this = $( this ),
1394 $parent = $this.parent(),
1395 externalLink = $this.data( 'fs-external-url' ),
1396 isOpensInNewTab = $this.data( 'fs-new-tab' );
1397
1398 if ( externalLink ) {
1399 $parent.attr( 'href', externalLink );
1400 }
1401
1402 if ( isOpensInNewTab ) {
1403 $parent.attr( { target: '_blank', rel: 'noopener noreferrer' } );
1404 }
1405 } );
1406 } )( jQuery );
1407 </script>
1408 <?php
1409 }
1410
1411 /**
1412 * @author Vova Feldman (@svovaf)
1413 * @since 1.0.9
1414 */
1415 private function register_constructor_hooks() {
1416 $this->_logger->entrance();
1417
1418 if ( is_admin() ) {
1419 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1420
1421 if ( $this->is_plugin() ) {
1422 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1423 /**
1424 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1425 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1426 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1427 * updating of a .org plugin).
1428 */
1429 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1430 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1431 /**
1432 * 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.
1433 *
1434 * @author Leo Fajardo (@leorw)
1435 * @since 2.2.3
1436 */
1437 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1438 }
1439
1440 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1441
1442 /**
1443 * @since 1.2.2
1444 *
1445 * Hook to both free and premium version activations to support
1446 * auto deactivation on the other version activation.
1447 */
1448 register_activation_hook(
1449 $plugin_dir . $this->_free_plugin_basename,
1450 array( &$this, '_activate_plugin_event_hook' )
1451 );
1452
1453 register_activation_hook(
1454 $plugin_dir . $this->premium_plugin_basename(),
1455 array( &$this, '_activate_plugin_event_hook' )
1456 );
1457 } else {
1458 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1459
1460 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1461 }
1462
1463 /**
1464 * Part of the mechanism to identify new plugin install vs. plugin update.
1465 *
1466 * @author Vova Feldman (@svovaf)
1467 * @since 1.1.9
1468 */
1469 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1470 /**
1471 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1472 * when the logic gets here since the activation logic first add the activate plugins,
1473 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1474 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1475 * plugin activation, and that IS intentional.
1476 *
1477 * @author Vova Feldman (@svovaf)
1478 */
1479 if ( $this->is_plugin() &&
1480 $this->is_activation_mode( false ) &&
1481 0 == did_action( 'plugins_loaded' )
1482 ) {
1483 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1484 } else {
1485 // If was activated before, then it was already loaded before.
1486 $this->_plugins_loaded();
1487 }
1488 }
1489
1490 add_action( 'plugins_loaded', array( &$this, '_run_garbage_collector' ) );
1491
1492 if ( ! self::is_ajax() ) {
1493 if ( ! $this->is_addon() ) {
1494 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1495 }
1496 }
1497
1498 if ( $this->_storage->handle_gdpr_admin_notice ) {
1499 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1500 }
1501
1502 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1503 add_action( 'init', array( &$this, '_add_pricing_ajax_handler' ) );
1504 }
1505
1506 if ( $this->is_plugin() ) {
1507 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1508 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1509 } else {
1510 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1511 }
1512
1513 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1514 }
1515
1516 if ( is_multisite() ) {
1517 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1518 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1519 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1520
1521 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1522 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1523 } else {
1524 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1525 }
1526
1527 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1528 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1529 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1530 }
1531
1532 if ( $this->is_theme() &&
1533 self::is_customizer() &&
1534 $this->apply_filters( 'show_customizer_upsell', true )
1535 ) {
1536 // Register customizer upsell.
1537 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1538 }
1539
1540 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1541
1542 if ( $this->is_theme() && ! $this->is_migration() ) {
1543 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1544 }
1545
1546 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1547 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1548 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1549 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1550 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1551
1552 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1553 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1554 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1555 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1556
1557 if ( $this->_is_network_active && fs_is_network_admin() ) {
1558 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1559 }
1560
1561 $this->add_ajax_action( 'install_premium_version', array(
1562 &$this,
1563 '_install_premium_version_ajax_action'
1564 ) );
1565
1566 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1567
1568 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1569
1570 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1571
1572 $this->add_action(
1573 'plugin_version_update',
1574 array( &$this, '_after_version_update' ),
1575 WP_FS__DEFAULT_PRIORITY,
1576 2
1577 );
1578 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1579
1580 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) ); // @phpstan-ignore-line
1581 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
1582 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1583
1584 /**
1585 * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
1586 *
1587 * @author Vova Feldman (@svovaf)
1588 * @since 1.2.1.5
1589 */
1590 if (
1591 (
1592 fs_request_is_action( 'reset_anonymous_mode' ) ||
1593 fs_request_is_action( 'reset_pending_activation_mode' )
1594 ) &&
1595 $this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' )
1596 ) {
1597 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1598 }
1599
1600 FS_DebugManager::register_hooks();
1601 }
1602
1603 /**
1604 * Register the required hooks right after the settings parse is completed.
1605 *
1606 * @author Vova Feldman (@svovaf)
1607 * @since 2.3.1
1608 */
1609 private function register_after_settings_parse_hooks() {
1610 if ( is_admin() &&
1611 $this->is_theme() &&
1612 $this->is_premium() &&
1613 ! $this->has_active_valid_license()
1614 ) {
1615 $this->add_ajax_action(
1616 'delete_theme_update_data',
1617 array( &$this, '_delete_theme_update_data_action' )
1618 );
1619 }
1620
1621 if ( $this->show_settings_with_tabs() ) {
1622 /**
1623 * Include the required hooks to capture the theme settings' page tabs
1624 * and cache them.
1625 *
1626 * @author Vova Feldman (@svovaf)
1627 * @since 1.2.2.7
1628 */
1629 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1630 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1631 // Add license activation AJAX callback.
1632 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1633
1634 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1635 }
1636
1637 add_action(
1638 'admin_footer',
1639 array( &$this, '_add_freemius_tabs' ),
1640 /**
1641 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1642 * That's why the priority is 11 while the tabs capture logic is added
1643 * with priority 10.
1644 *
1645 * @author Vova Feldman (@svovaf)
1646 */
1647 11
1648 );
1649 }
1650
1651 if ( ! self::is_ajax() ) {
1652 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1653 add_action(
1654 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1655 array( &$this, '_prepare_admin_menu' ),
1656 WP_FS__LOWEST_PRIORITY
1657 );
1658 }
1659 }
1660
1661 if (
1662 $this->is_user_in_admin() &&
1663 $this->is_parallel_activation() &&
1664 $this->_premium_plugin_basename !== $this->_premium_plugin_basename_from_parallel_activation
1665 ) {
1666 $this->_premium_plugin_basename = $this->_premium_plugin_basename_from_parallel_activation;
1667
1668 register_activation_hook(
1669 dirname( $this->_plugin_dir_path ) . '/' . $this->_premium_plugin_basename,
1670 array( &$this, '_activate_plugin_event_hook' )
1671 );
1672 }
1673 }
1674
1675 /**
1676 * Determines if a plugin is running in parallel activation mode.
1677 *
1678 * @author Leo Fajardo (@leorw)
1679 * @since 2.9.1
1680 *
1681 * @return bool
1682 */
1683 private function is_parallel_activation() {
1684 return ! empty( $this->_premium_plugin_basename_from_parallel_activation );
1685 }
1686
1687 /**
1688 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1689 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1690 *
1691 * @author Leo Fajardo (@leorw)
1692 * @since 2.2.3
1693 *
1694 * @param object $updates
1695 * @param string|null $transient
1696 *
1697 * @return object
1698 */
1699 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1700 if ( is_object( $updates ) && isset( $updates->response ) ) {
1701 foreach ( $updates->response as $file => $plugin ) {
1702 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1703 unset( $updates->response[ $file ] );
1704 }
1705 }
1706 }
1707
1708 return $updates;
1709 }
1710
1711 /**
1712 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1713 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1714 *
1715 * @author Leo Fajardo (@leorw)
1716 * @since 2.2.3
1717 *
1718 * @return void
1719 */
1720 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1721 $slug_basename_map = array();
1722 foreach ( self::$_instances as $instance ) {
1723 if ( ! $instance->is_plugin() ) {
1724 continue;
1725 }
1726
1727 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1728 }
1729 ?>
1730 <script type="text/javascript">
1731 (function( $ ) {
1732 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1733 for ( var slug in slugBasenameMap ) {
1734 var basename = slugBasenameMap[ slug ];
1735
1736 // Try to get the plugin rows if on the "Plugins" page.
1737 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1738
1739 if ( 0 === $pluginRows.length ) {
1740 // Try to get the plugin rows if on the "Updates" page.
1741 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1742 if ( 0 !== $pluginCheckbox.length ) {
1743 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1744 }
1745 }
1746
1747 if ( 0 === $pluginRows.length ) {
1748 // No plugin rows found.
1749 continue;
1750 }
1751
1752 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1753 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1754 var $this = $( this ),
1755 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1756
1757 $this.attr( 'href', href );
1758 });
1759 }
1760 })( jQuery );
1761 </script>
1762 <?php
1763 }
1764
1765 /**
1766 * @author Leo Fajardo (@leorw)
1767 * @since 2.3.0
1768 */
1769 static function _maybe_add_beta_label_styles() {
1770 $has_any_beta_version = false;
1771
1772 foreach ( self::$_instances as $instance ) {
1773 if ( $instance->is_beta() ) {
1774 $has_any_beta_version = true;
1775 break;
1776 }
1777 }
1778
1779 if ( $has_any_beta_version ) {
1780 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1781 }
1782 }
1783
1784 /**
1785 * @author Leo Fajardo (@leorw)
1786 * @since 2.3.0
1787 */
1788 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1789 $beta_data = array();
1790
1791 foreach ( self::$_instances as $instance ) {
1792 if ( ! $instance->is_premium() ) {
1793 continue;
1794 }
1795
1796 /**
1797 * If there's an available beta version update, a confirmation message will be shown when the
1798 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1799 */
1800 $has_beta_update = $instance->has_beta_update();
1801
1802 $is_beta = (
1803 // The "Beta" label is added separately for themes.
1804 $instance->is_plugin() &&
1805 $instance->is_beta()
1806 );
1807
1808 if ( ! $is_beta && ! $has_beta_update ) {
1809 continue;
1810 }
1811
1812 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1813
1814 if ( ! $has_beta_update ) {
1815 continue;
1816 }
1817
1818 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1819 '%s %s',
1820 sprintf(
1821 fs_esc_attr_inline(
1822 '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.',
1823 'beta-version-update-caution',
1824 $instance->get_slug()
1825 ),
1826 $instance->get_plugin_title()
1827 ),
1828 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1829 );
1830 }
1831
1832 if ( empty( $beta_data ) ) {
1833 return;
1834 }
1835 ?>
1836 <script type="text/javascript">
1837 ( function( $ ) {
1838 var betaData = <?php echo json_encode( $beta_data ) ?>;
1839
1840 for ( var pluginBasename in betaData ) {
1841 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1842 continue;
1843 }
1844
1845 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1846 continue;
1847 }
1848
1849 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1850 if ( 0 === $parentContainer.length ) {
1851 continue;
1852 }
1853
1854 $parentContainer.find( '.plugin-title > strong:first-child').append(
1855 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1856 );
1857 }
1858
1859 setTimeout( function() {
1860 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1861 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1862 var $parentContainer = $( this ).parents( 'tr:first' );
1863 pluginBasename = ( 0 !== $parentContainer.length ) ?
1864 $parentContainer.data( 'plugin' ) :
1865 $( this ).parents( '.theme:first' ).data( 'slug' );
1866
1867 if (
1868 betaData[ pluginBasename ] &&
1869 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1870 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1871 ) {
1872 return false;
1873 }
1874 } );
1875 }, 20 );
1876 } )( jQuery );
1877 </script>
1878 <?php
1879 }
1880
1881 /**
1882 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1883 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1884 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1885 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1886 *
1887 * @author Leo Fajardo (@leorw)
1888 *
1889 * @since 1.2.0
1890 */
1891 private function unregister_uninstall_hook() {
1892 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1893 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1894 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1895
1896 update_option( 'uninstall_plugins', $uninstallable_plugins );
1897 }
1898
1899 /**
1900 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1901 *
1902 * @param bool $store_prev_path
1903 */
1904 private function clear_module_main_file_cache( $store_prev_path = true ) {
1905 if ( ! isset( $this->_storage->plugin_main_file ) ||
1906 empty( $this->_storage->plugin_main_file->path )
1907 ) {
1908 return;
1909 }
1910
1911 if ( ! $store_prev_path ) {
1912 /**
1913 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1914 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1915 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1916 *
1917 * @author Leo Fajardo (@leorw)
1918 * @since 2.2.1
1919 */
1920 unset( $this->_storage->plugin_main_file->path );
1921 } else {
1922 $plugin_main_file = clone $this->_storage->plugin_main_file;
1923
1924 // Store cached path (2nd layer cache).
1925 $plugin_main_file->prev_path = $plugin_main_file->path;
1926
1927 // Clear cached path.
1928 unset( $plugin_main_file->path );
1929
1930 $this->_storage->plugin_main_file = $plugin_main_file;
1931 }
1932
1933 /**
1934 * Clear global cached path.
1935 *
1936 * @author Leo Fajardo (@leorw)
1937 * @since 1.2.2
1938 */
1939 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1940 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1941 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1942 }
1943
1944 /**
1945 * @author Leo Fajardo (@leorw)
1946 * @since 2.0.0
1947 */
1948 function _hook_action_links_and_register_account_hooks() {
1949 if ( $this->is_migration() ) {
1950 return;
1951 }
1952
1953 if (
1954 ( self::is_plugins_page() && $this->is_plugin() ) ||
1955 ( self::is_themes_page() && $this->is_theme() ) ||
1956 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1957 ) {
1958 $this->_add_tracking_links();
1959 }
1960
1961 if ( self::is_plugins_page() && $this->is_plugin() ) {
1962 $this->hook_plugin_action_links();
1963 }
1964
1965 $this->_register_account_hooks();
1966 }
1967
1968 /**
1969 * @author Vova Feldman (@svovaf)
1970 * @since 1.0.9
1971 */
1972 private function _register_account_hooks() {
1973 if ( ! is_admin() ) {
1974 return;
1975 }
1976
1977 /**
1978 * Always show the deactivation feedback form since we added
1979 * automatic free version deactivation upon premium code activation.
1980 *
1981 * @since 1.2.1.6
1982 */
1983 $this->add_ajax_action(
1984 'submit_uninstall_reason',
1985 array( &$this, '_submit_uninstall_reason_action' )
1986 );
1987
1988 $this->add_ajax_action(
1989 'cancel_subscription_or_trial',
1990 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1991 );
1992
1993 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1994 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1995 ( $this->is_theme() && self::is_themes_page() )
1996 ) {
1997 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1998 }
1999 }
2000 }
2001
2002 /**
2003 * Leverage backtrace to find caller plugin file path.
2004 *
2005 * @param bool $is_init Is initiation sequence.
2006 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
2007 *
2008 * @return string
2009 * @since 1.0.6
2010 *
2011 * @author Vova Feldman (@svovaf)
2012 */
2013 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
2014 // Try to load the cached value of the file path.
2015 if ( isset( $this->_storage->plugin_main_file ) ) {
2016 $plugin_main_file = $this->_storage->plugin_main_file;
2017 if ( ! empty( $plugin_main_file->path ) ) {
2018 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
2019 if ( file_exists( $absolute_path ) ) {
2020 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
2021 // Update cached path if not matching the actual path.
2022 $plugin_main_file->path = $main_file;
2023 $this->_storage->plugin_main_file = $plugin_main_file;
2024 }
2025
2026 return $absolute_path;
2027 }
2028 }
2029 }
2030
2031 /**
2032 * @since 1.2.1
2033 *
2034 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
2035 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
2036 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
2037 *
2038 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
2039 * when the class instantiator isn't the module.
2040 */
2041 if ( ! $is_init ) {
2042 // Fetch prev path cache.
2043 if ( isset( $this->_storage->plugin_main_file ) &&
2044 ! empty( $this->_storage->plugin_main_file->prev_path )
2045 ) {
2046 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
2047 if ( file_exists( $absolute_path ) ) {
2048 return $absolute_path;
2049 }
2050 }
2051
2052 wp_die(
2053 $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' ) .
2054 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2055 $this->get_text_inline( 'Error', 'error' ),
2056 array( 'back_link' => true )
2057 );
2058 }
2059
2060 /**
2061 * @since 1.2.1
2062 *
2063 * Only the original instantiator that calls dynamic_init can modify the module's path.
2064 */
2065 // Find caller module.
2066 $this->_storage->plugin_main_file = (object) array(
2067 'path' => $main_file,
2068 );
2069
2070 return $this->get_absolute_path( $main_file );
2071 }
2072
2073 /**
2074 * @author Leo Fajardo (@leorw)
2075 * @since 1.2.3
2076 *
2077 * @param string $path
2078 *
2079 * @return string
2080 */
2081 private function get_relative_path( $path ) {
2082 $module_root_dir = $this->get_module_root_dir_path();
2083 if ( 0 === strpos( $path, $module_root_dir ) ) {
2084 $path = substr( $path, strlen( $module_root_dir ) );
2085 }
2086
2087 return $path;
2088 }
2089
2090 /**
2091 * @author Leo Fajardo (@leorw)
2092 * @since 1.2.3
2093 *
2094 * @param string $path
2095 * @param string|bool $module_type
2096 *
2097 * @return string
2098 */
2099 private function get_absolute_path( $path, $module_type = false ) {
2100 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2101 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2102 $path = fs_normalize_path( $module_root_dir . $path );
2103 }
2104
2105 return $path;
2106 }
2107
2108 /**
2109 * @author Leo Fajardo (@leorw)
2110 * @since 1.2.3
2111 *
2112 * @param string|bool $module_type
2113 *
2114 * @return string
2115 */
2116 private function get_module_root_dir_path( $module_type = false ) {
2117 $is_plugin = empty( $module_type ) ?
2118 $this->is_plugin() :
2119 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2120
2121 return fs_normalize_path( trailingslashit( $is_plugin ?
2122 WP_PLUGIN_DIR :
2123 get_theme_root( get_stylesheet() ) ) );
2124 }
2125
2126 /**
2127 * @author Leo Fajardo (@leorw)
2128 *
2129 * @param number $module_id
2130 * @param string $slug
2131 *
2132 * @return string Since 2.5.0 return the module's main file path.
2133 *
2134 * @since 1.2.2
2135 */
2136 private function store_id_slug_type_path_map( $module_id, $slug ) {
2137 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2138
2139 $store_option = false;
2140
2141 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2142 $id_slug_type_path_map[ $module_id ] = array(
2143 'slug' => $slug
2144 );
2145
2146 $store_option = true;
2147 } else if (
2148 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2149 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2150 ) {
2151 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2152 $store_option = true;
2153 }
2154
2155 $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
2156
2157 if ( ! $find_caller ) {
2158 /**
2159 * This verification is for cases when suddenly the same module
2160 * is installed but with a different folder name.
2161 *
2162 * @author Vova Feldman (@svovaf)
2163 * @since 1.2.3
2164 */
2165 $find_caller = ! file_exists( $this->get_absolute_path(
2166 $id_slug_type_path_map[ $module_id ]['path'],
2167 $id_slug_type_path_map[ $module_id ]['type']
2168 ) );
2169 }
2170
2171 foreach ( $id_slug_type_path_map as $id => $data ) {
2172 if ( empty( $id ) ) {
2173 // Remove maps with empty module ID.
2174 unset( $id_slug_type_path_map[ $id ] );
2175 $store_option = true;
2176 continue;
2177 }
2178
2179 /**
2180 * If the module's main file path is identical to the main file path of another module then it means that the cached path of the current module or the other one with the same path is wrong, and therefore, we need to recalculate those paths.
2181 *
2182 * @author Vova Feldman (@svovaf)
2183 * @since 2.5.0
2184 */
2185 if ( ! $find_caller ) {
2186 if ( $id == $module_id ) {
2187 continue;
2188 }
2189
2190 if (
2191 isset( $data['path'] ) &&
2192 $data['path'] === $id_slug_type_path_map[ $module_id ]['path']
2193 ) {
2194 $find_caller = true;
2195 }
2196 }
2197 }
2198
2199 if ( $find_caller ) {
2200 $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
2201
2202 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2203 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2204
2205 $store_option = true;
2206 }
2207
2208 if ( $store_option ) {
2209 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2210 }
2211
2212 return $id_slug_type_path_map[ $module_id ]['path'];
2213 }
2214
2215 /**
2216 * Identifies the caller type: plugin or theme.
2217 *
2218 * @author Leo Fajardo (@leorw)
2219 * @since 1.2.2
2220 *
2221 * @author Vova Feldman (@svovaf)
2222 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2223 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2224 * SDK an internal file instead of directly from functions.php.
2225 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2226 *
2227 * @param number $module_id @since 2.5.0
2228 */
2229 private function get_caller_main_file_and_type( $module_id ) {
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 $caller_main_file_and_type = (object) array(
2369 'module_type' => $module_type,
2370 'path' => $caller_file_candidate
2371 );
2372
2373 return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
2374 }
2375
2376 #----------------------------------------------------------------------------------
2377 #region Deactivation Feedback Form
2378 #----------------------------------------------------------------------------------
2379
2380 /**
2381 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2382 * page.
2383 *
2384 * @author Vova Feldman (@svovaf)
2385 * @author Leo Fajardo (@leorw)
2386 *
2387 * @since 1.1.2
2388 */
2389 function _add_deactivation_feedback_dialog_box() {
2390 if (
2391 $this->is_clone() ||
2392 ( is_object( $this->_site ) && ! $this->is_registered() )
2393 ) {
2394 return;
2395 }
2396
2397 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2398 $this->_get_subscription_cancellation_dialog_box_template_params() :
2399 array();
2400
2401 /**
2402 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2403 */
2404 $show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
2405 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2406 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2407 } else if ( $this->is_addon() ) {
2408 /**
2409 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2410 */
2411 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2412 }
2413
2414 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2415
2416 if (
2417 empty( $subscription_cancellation_dialog_box_template_params ) &&
2418 ! $show_deactivation_feedback_form &&
2419 empty( $uninstall_confirmation_message )
2420 ) {
2421 return;
2422 }
2423
2424 $vars = array( 'id' => $this->_module_id );
2425
2426 if ( $show_deactivation_feedback_form ) {
2427 /* Check the type of user:
2428 * 1. Long-term (long-term)
2429 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2430 * 3. Short-term (short-term)
2431 */
2432 $is_long_term_user = true;
2433
2434 // Check if the site is at least 2 days old.
2435 $time_installed = $this->_storage->install_timestamp;
2436
2437 // Difference in seconds.
2438 $date_diff = time() - $time_installed;
2439
2440 // Convert seconds to days.
2441 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2442
2443 if ( $date_diff_days < 2 ) {
2444 $is_long_term_user = false;
2445 }
2446
2447 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2448
2449 if ( $is_long_term_user ) {
2450 $user_type = 'long-term';
2451 } else {
2452 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2453 $user_type = 'non-registered-and-non-anonymous-short-term';
2454 } else {
2455 $user_type = 'short-term';
2456 }
2457 }
2458
2459 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2460
2461 $vars['reasons'] = $uninstall_reasons;
2462 }
2463
2464 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2465 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2466 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2467
2468 /**
2469 * Load the HTML template for the deactivation feedback dialog box.
2470 *
2471 * @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.
2472 */
2473 fs_require_template( 'forms/deactivation/form.php', $vars );
2474 }
2475
2476 /**
2477 * @author Leo Fajardo (@leorw)
2478 * @since 1.1.2
2479 *
2480 * @param string $user_type
2481 *
2482 * @return array The uninstall reasons for the specified user type.
2483 */
2484 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2485 $module_type = $this->_module_type;
2486
2487 $internal_message_template_var = array(
2488 'id' => $this->_module_id
2489 );
2490
2491 $plan = $this->get_plan();
2492
2493 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2494 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2495 } else {
2496 $contact_support_template = '';
2497 }
2498
2499 $reason_found_better_plugin = array(
2500 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2501 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2502 'input_type' => 'textfield',
2503 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2504 );
2505
2506 $reason_temporary_deactivation = array(
2507 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2508 'text' => sprintf(
2509 $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
2510 strtolower( $this->is_plugin() ?
2511 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2512 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2513 )
2514 ),
2515 'input_type' => '',
2516 'input_placeholder' => ''
2517 );
2518
2519 $reason_other = array(
2520 'id' => self::REASON_OTHER,
2521 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2522 'input_type' => 'textfield',
2523 'input_placeholder' => ''
2524 );
2525
2526 $long_term_user_reasons = array(
2527 array(
2528 'id' => self::REASON_NO_LONGER_NEEDED,
2529 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2530 'input_type' => '',
2531 'input_placeholder' => ''
2532 ),
2533 $reason_found_better_plugin,
2534 array(
2535 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2536 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2537 'input_type' => '',
2538 'input_placeholder' => ''
2539 ),
2540 array(
2541 'id' => self::REASON_BROKE_MY_SITE,
2542 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2543 'input_type' => '',
2544 'input_placeholder' => '',
2545 'internal_message' => $contact_support_template
2546 ),
2547 array(
2548 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2549 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2550 'input_type' => '',
2551 'input_placeholder' => '',
2552 'internal_message' => $contact_support_template
2553 )
2554 );
2555
2556 if ( $this->is_paying() ) {
2557 $long_term_user_reasons[] = array(
2558 'id' => self::REASON_CANT_PAY_ANYMORE,
2559 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2560 'input_type' => 'textfield',
2561 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2562 );
2563 }
2564
2565 $reason_dont_share_info = array(
2566 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2567 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2568 'input_type' => '',
2569 'input_placeholder' => ''
2570 );
2571
2572 /**
2573 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2574 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2575 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2576 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2577 * button in the opt-in form is shown/hidden).
2578 */
2579 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2580 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2581 }
2582
2583 $uninstall_reasons = array(
2584 'long-term' => $long_term_user_reasons,
2585 'non-registered-and-non-anonymous-short-term' => array(
2586 array(
2587 'id' => self::REASON_DIDNT_WORK,
2588 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2589 'input_type' => '',
2590 'input_placeholder' => ''
2591 ),
2592 $reason_dont_share_info,
2593 $reason_found_better_plugin
2594 ),
2595 'short-term' => array(
2596 array(
2597 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2598 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2599 'input_type' => '',
2600 'input_placeholder' => '',
2601 'internal_message' => $contact_support_template
2602 ),
2603 $reason_found_better_plugin,
2604 array(
2605 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2606 '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 ),
2607 'input_type' => 'textarea',
2608 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2609 ),
2610 array(
2611 'id' => self::REASON_NOT_WORKING,
2612 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2613 'input_type' => 'textarea',
2614 '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' )
2615 ),
2616 array(
2617 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2618 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2619 'input_type' => 'textarea',
2620 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2621 ),
2622 array(
2623 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2624 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2625 'input_type' => 'textarea',
2626 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2627 )
2628 )
2629 );
2630
2631 // Randomize the reasons for the current user type.
2632 shuffle( $uninstall_reasons[ $user_type ] );
2633
2634 // Keep the following reasons as the last items in the list.
2635 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2636 $uninstall_reasons[ $user_type ][] = $reason_other;
2637
2638 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2639
2640 return $uninstall_reasons[ $user_type ];
2641 }
2642
2643 /**
2644 * Called after the user has submitted his reason for deactivating the plugin.
2645 *
2646 * @author Leo Fajardo (@leorw)
2647 * @since 1.1.2
2648 */
2649 function _submit_uninstall_reason_action() {
2650 $this->_logger->entrance();
2651
2652 $this->check_ajax_referer( 'submit_uninstall_reason' );
2653
2654 $reason_id = fs_request_get( 'reason_id' );
2655
2656 // Check if the given reason ID is an unsigned integer.
2657 if ( ! ctype_digit( $reason_id ) ) {
2658 exit;
2659 }
2660
2661 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2662 if ( ! empty( $reason_info ) ) {
2663 $reason_info = substr( $reason_info, 0, 128 );
2664 }
2665
2666 $reason = (object) array(
2667 'id' => $reason_id,
2668 'info' => $reason_info,
2669 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2670 );
2671
2672 $this->_storage->store( 'uninstall_reason', $reason );
2673
2674 if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) {
2675 $snooze_period = fs_request_get( 'snooze_period' );
2676
2677 if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) {
2678 self::snooze_deactivation_form( (int) $snooze_period );
2679 }
2680 }
2681
2682 /**
2683 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2684 * not support uninstall hook.
2685 *
2686 * @author Leo Fajardo (@leorw)
2687 * @since 1.2.2
2688 */
2689 if ( $this->is_theme() ) {
2690 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2691 FS_Plugin_Updater::instance( $this )->delete_update_data();
2692 }
2693
2694 $this->_uninstall_plugin_event( false );
2695 $this->remove_sdk_reference();
2696 }
2697
2698 // Print '1' for successful operation.
2699 echo 1;
2700 exit;
2701 }
2702
2703 #--------------------------------------------------------------------------------
2704 #region Deactivation Feedback Snoozing
2705 #--------------------------------------------------------------------------------
2706
2707 /**
2708 * @author Vova Feldman (@svovaf)
2709 * @since 2.4.3
2710 *
2711 * @param int $period
2712 *
2713 * @return bool True if the value was set, false otherwise.
2714 */
2715 private static function snooze_deactivation_form( $period ) {
2716 return ( 0 < $period && self::reset_deactivation_snoozing( $period ) );
2717 }
2718
2719 /**
2720 * Check if deactivation feedback form is snoozed.
2721 *
2722 * @author Vova Feldman (@svovaf)
2723 * @since 2.4.3
2724 *
2725 * @return bool
2726 */
2727 static function is_deactivation_snoozed() {
2728 $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ?
2729 get_transient( 'fs_snooze_period' ) :
2730 get_site_transient( 'fs_snooze_period' );
2731
2732
2733 return ( 'true' === $is_snoozed );
2734 }
2735
2736 /**
2737 * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period.
2738 *
2739 * @param int $period Period in seconds.
2740 *
2741 * @author Vova Feldman (@svovaf)
2742 * @since 2.4.3
2743 */
2744 public static function reset_deactivation_snoozing( $period = 0 ) {
2745 $value = ( 0 === $period ) ? null : 'true';
2746
2747 if ( ! is_multisite() || fs_is_network_admin() ) {
2748 return set_transient( 'fs_snooze_period', $value, $period );
2749 } else {
2750 return set_site_transient( 'fs_snooze_period', $value, $period );
2751 }
2752 }
2753
2754 /**
2755 * The deactivation snooze expiration UNIX timestamp (in sec).
2756 *
2757 * @author Vova Feldman (@svovaf)
2758 * @since 2.4.3
2759 *
2760 * @return int
2761 */
2762 static function deactivation_snooze_expires_at() {
2763 return ( ! is_multisite() || fs_is_network_admin() ) ?
2764 (int) get_option( '_transient_timeout_fs_snooze_period' ) :
2765 (int) get_site_option( '_site_transient_timeout_fs_snooze_period' );
2766 }
2767
2768 #endregion
2769
2770 /**
2771 * @author Leo Fajardo (@leorw)
2772 * @since 2.1.4
2773 */
2774 function cancel_subscription_or_trial_ajax_action() {
2775 $this->_logger->entrance();
2776
2777 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2778
2779 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2780
2781 if ( $this->is_api_error( $result ) ) {
2782 $this->shoot_ajax_failure( $result->error->message );
2783 }
2784
2785 $this->shoot_ajax_success();
2786 }
2787
2788 /**
2789 * @author Leo Fajardo (@leorw)
2790 * @since 2.1.4
2791 *
2792 * @param number $plugin_id
2793 *
2794 * @return object
2795 */
2796 private function cancel_subscription_or_trial( $plugin_id ) {
2797 $fs = null;
2798 if ( $plugin_id == $this->get_id() ) {
2799 $fs = $this;
2800 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2801 $fs = self::get_instance_by_id( $plugin_id );
2802 }
2803
2804 $result = null;
2805
2806 if ( ! is_null( $fs ) ) {
2807 $result = $fs->is_paid_trial() ?
2808 $fs->_cancel_trial() :
2809 $fs->_downgrade_site();
2810 }
2811
2812 return $result;
2813 }
2814
2815 /**
2816 * @author Leo Fajardo (@leorw)
2817 * @since 2.0.2
2818 */
2819 function _delete_theme_update_data_action() {
2820 FS_Plugin_Updater::instance( $this )->delete_update_data();
2821 }
2822
2823 #endregion
2824
2825 #----------------------------------------------------------------------------------
2826 #region Instance
2827 #----------------------------------------------------------------------------------
2828
2829 /**
2830 * Main singleton instance.
2831 *
2832 * @author Vova Feldman (@svovaf)
2833 * @since 1.0.0
2834 *
2835 * @param number $module_id
2836 * @param string|bool $slug
2837 * @param bool $is_init Is initiation sequence.
2838 *
2839 * @return Freemius|false
2840 */
2841 static function instance( $module_id, $slug = false, $is_init = false ) {
2842 if ( empty( $module_id ) ) {
2843 return false;
2844 }
2845
2846 /**
2847 * 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.
2848 */
2849 self::_load_required_static();
2850
2851 if ( ! is_numeric( $module_id ) ) {
2852 if ( ! $is_init && true === $slug ) {
2853 $is_init = true;
2854 }
2855
2856 $slug = $module_id;
2857
2858 $module = FS_Plugin_Manager::instance( $slug )->get();
2859
2860 if ( is_object( $module ) ) {
2861 $module_id = $module->id;
2862 }
2863 }
2864
2865 $key = 'm_' . $module_id;
2866
2867 if ( ! isset( self::$_instances[ $key ] ) ) {
2868 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2869 }
2870
2871 return self::$_instances[ $key ];
2872 }
2873
2874 /**
2875 * @author Vova Feldman (@svovaf)
2876 * @since 1.0.6
2877 *
2878 * @param number $addon_id
2879 *
2880 * @return bool
2881 */
2882 private static function has_instance( $addon_id ) {
2883 return isset( self::$_instances[ 'm_' . $addon_id ] );
2884 }
2885
2886 /**
2887 * @author Leo Fajardo (@leorw)
2888 * @since 1.2.2
2889 *
2890 * @param string|number $id_or_slug
2891 * @param string $module_type
2892 *
2893 * @return number|false
2894 */
2895 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2896 if ( is_numeric( $id_or_slug ) ) {
2897 return $id_or_slug;
2898 }
2899
2900 foreach ( self::$_instances as $instance ) {
2901 // Also check the module type since there can be a plugin and a theme with the same slug.
2902 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2903 return $instance->get_id();
2904 }
2905 }
2906
2907 return false;
2908 }
2909
2910 /**
2911 * @author Vova Feldman (@svovaf)
2912 * @since 1.0.6
2913 *
2914 * @param number $id
2915 *
2916 * @return false|Freemius
2917 */
2918 static function get_instance_by_id( $id ) {
2919 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2920 self::$_instances[ 'm_' . $id ] :
2921 false;
2922 }
2923
2924 /**
2925 *
2926 * @author Vova Feldman (@svovaf)
2927 * @since 1.0.1
2928 *
2929 * @param string $plugin_file
2930 * @param string $module_type
2931 *
2932 * @return false|Freemius
2933 */
2934 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2935 $slug = self::find_slug_by_basename( $plugin_file );
2936
2937 return ( false !== $slug ) ?
2938 self::instance( self::get_module_id( $slug, $module_type ) ) :
2939 false;
2940 }
2941
2942 /**
2943 * @author Vova Feldman (@svovaf)
2944 * @since 1.0.6
2945 *
2946 * @return false|Freemius
2947 */
2948 function get_parent_instance() {
2949 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2950 }
2951
2952 /**
2953 * @author Vova Feldman (@svovaf)
2954 * @since 1.0.6
2955 *
2956 * @param string|number $id_or_slug
2957 *
2958 * @return false|Freemius
2959 */
2960 function get_addon_instance( $id_or_slug ) {
2961 $addon_id = self::get_module_id( $id_or_slug );
2962
2963 return self::instance( $addon_id );
2964 }
2965
2966 /**
2967 * @return Freemius[]
2968 */
2969 static function _get_all_instances() {
2970 return self::$_instances;
2971 }
2972
2973 #endregion ------------------------------------------------------------------
2974
2975 /**
2976 * @author Vova Feldman (@svovaf)
2977 * @since 1.0.6
2978 *
2979 * @return bool
2980 */
2981 function is_parent_plugin_installed() {
2982 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2983
2984 if ( $is_active ) {
2985 return true;
2986 }
2987
2988 /**
2989 * Parent module might be a theme. If that's the case, the add-on's FS
2990 * instance will be loaded prior to the theme's FS instance, therefore,
2991 * we need to check if it's active with a "look ahead".
2992 *
2993 * @author Vova Feldman
2994 * @since 1.2.2.3
2995 */
2996 global $fs_active_plugins;
2997 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2998 $active_theme = wp_get_theme();
2999
3000 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
3001 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
3002 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
3003 // Parent module is a theme and it's currently active.
3004 return true;
3005 }
3006 }
3007 }
3008 }
3009
3010 return false;
3011 }
3012
3013 /**
3014 * Check if add-on parent plugin in activation mode.
3015 *
3016 * @author Vova Feldman (@svovaf)
3017 * @since 1.0.7
3018 *
3019 * @return bool
3020 */
3021 function is_parent_in_activation() {
3022 $parent_fs = $this->get_parent_instance();
3023 if ( ! is_object( $parent_fs ) ) {
3024 return false;
3025 }
3026
3027 return ( $parent_fs->is_activation_mode() );
3028 }
3029
3030 /**
3031 * Is plugin in activation mode.
3032 *
3033 * @author Vova Feldman (@svovaf)
3034 * @since 1.0.7
3035 *
3036 * @param bool $and_on
3037 *
3038 * @return bool
3039 */
3040 function is_activation_mode( $and_on = true ) {
3041 return fs_is_network_admin() ?
3042 $this->is_network_activation_mode( $and_on ) :
3043 $this->is_site_activation_mode( $and_on );
3044 }
3045
3046 /**
3047 * Is plugin in activation mode.
3048 *
3049 * @author Vova Feldman (@svovaf)
3050 * @since 1.0.7
3051 *
3052 * @param bool $and_on
3053 *
3054 * @return bool
3055 */
3056 function is_site_activation_mode( $and_on = true ) {
3057 return (
3058 ( $this->is_on() || ! $and_on ) &&
3059 (
3060 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
3061 (
3062 ( ! $this->is_registered() ||
3063 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
3064 ( ! $this->is_enable_anonymous() ||
3065 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
3066 )
3067 )
3068 );
3069 }
3070
3071 /**
3072 * Checks if the SDK in network activation mode.
3073 *
3074 * @author Leo Fajardo (@leorw)
3075 * @since 2.0.0
3076 *
3077 * @param bool $and_on
3078 *
3079 * @return bool
3080 */
3081 private function is_network_activation_mode( $and_on = true ) {
3082 if ( ! $this->_is_network_active ) {
3083 // Not network activated.
3084 return false;
3085 }
3086
3087 if ( $this->is_network_upgrade_mode() ) {
3088 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3089 return true;
3090 }
3091
3092 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3093 // 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.
3094 return false;
3095 }
3096
3097 if ( $this->is_network_delegated_connection() ) {
3098 // Super-admin delegated the connection to the site admins -> not activation mode.
3099 return false;
3100 }
3101
3102 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3103 // Super-admin skipped the connection network wide -> not activation mode.
3104 return false;
3105 }
3106
3107 if ( $this->is_network_registered() ) {
3108 // Super-admin connected at least one site -> not activation mode.
3109 return false;
3110 }
3111
3112 return true;
3113 }
3114
3115 /**
3116 * Check if current page is the opt-in/pending-activation page.
3117 *
3118 * @author Vova Feldman (@svovaf)
3119 * @since 1.2.1.7
3120 *
3121 * @return bool
3122 */
3123 function is_activation_page() {
3124 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3125 return true;
3126 }
3127
3128 if ( ! $this->is_activation_mode() ) {
3129 return false;
3130 }
3131
3132 // Check if current page is matching the activation page.
3133 return $this->is_matching_url( $this->get_activation_url() );
3134 }
3135
3136 /**
3137 * Check if URL path's are matching and that all querystring
3138 * arguments of the $sub_url exist in the $url with the same values.
3139 *
3140 * WARNING:
3141 * 1. This method doesn't check if the sub/domain are matching.
3142 * 2. Ignore case sensitivity.
3143 *
3144 * @author Vova Feldman (@svovaf)
3145 * @since 1.2.1.7
3146 *
3147 * @param string $sub_url
3148 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3149 *
3150 * @return bool
3151 */
3152 private function is_matching_url( $sub_url, $url = '' ) {
3153 if ( empty( $url ) ) {
3154 $url = $_SERVER['REQUEST_URI'];
3155 }
3156
3157 $url = strtolower( $url );
3158 $sub_url = strtolower( $sub_url );
3159
3160 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3161 // Different path - DO NOT OVERRIDE PAGE.
3162 return false;
3163 }
3164
3165 $url_params = fs_parse_url_params( $url );
3166 $sub_url_params = fs_parse_url_params( $sub_url );
3167
3168 foreach ( $sub_url_params as $key => $val ) {
3169 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3170 // Not matching query string - DO NOT OVERRIDE PAGE.
3171 return false;
3172 }
3173 }
3174
3175 return true;
3176 }
3177
3178 /**
3179 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3180 *
3181 * @author Vova Feldman (@svovaf)
3182 * @since 2.0.0
3183 *
3184 * @param int $blog_id
3185 *
3186 * @return string[]
3187 */
3188 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3189 if ( is_multisite() && $blog_id > 0 ) {
3190 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3191 } else {
3192 $active_basenames = get_option( 'active_plugins' );
3193 }
3194
3195 if ( ! is_array( $active_basenames ) ) {
3196 $active_basenames = array();
3197 }
3198
3199 if ( is_multisite() ) {
3200 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3201
3202 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3203 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3204 }
3205 }
3206
3207 return $active_basenames;
3208 }
3209
3210 /**
3211 * @author Leo Fajardo (@leorw)
3212 * @since 2.3.0
3213 *
3214 * @param int $blog_id
3215 *
3216 * @return array
3217 */
3218 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3219 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3220
3221 $map = array();
3222
3223 foreach ( $active_basenames as $active_basename ) {
3224 $active_basename = fs_normalize_path( $active_basename );
3225
3226 if ( false === strpos( $active_basename, '/' ) ) {
3227 continue;
3228 }
3229
3230 $map[ dirname( $active_basename ) ] = true;
3231 }
3232
3233 return $map;
3234 }
3235
3236 /**
3237 * Get collection of all active plugins. Including network activated plugins.
3238 *
3239 * @author Vova Feldman (@svovaf)
3240 * @since 1.0.9
3241 *
3242 * @param int $blog_id Since 2.0.0
3243 *
3244 * @return array[string]array
3245 */
3246 private static function get_active_plugins( $blog_id = 0 ) {
3247 self::require_plugin_essentials();
3248
3249 $active_plugin = array();
3250 $all_plugins = fs_get_plugins();
3251 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3252
3253 foreach ( $active_plugins_basenames as $plugin_basename ) {
3254 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3255 }
3256
3257 return $active_plugin;
3258 }
3259
3260 /**
3261 * Get collection of all site active plugins for a specified blog.
3262 *
3263 * @author Vova Feldman (@svovaf)
3264 * @since 2.0.0
3265 *
3266 * @param int $blog_id
3267 *
3268 * @return array[string]array
3269 */
3270 private static function get_site_active_plugins( $blog_id = 0 ) {
3271 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3272 get_blog_option( $blog_id, 'active_plugins' ) :
3273 get_option( 'active_plugins' );
3274
3275 $active = array();
3276
3277 if ( ! is_array( $active_basenames ) ) {
3278 return $active;
3279 }
3280
3281 foreach ( $active_basenames as $basename ) {
3282 $active[ $basename ] = array(
3283 'is_active' => true,
3284 'Version' => '1.0', // Dummy version.
3285 'slug' => self::get_plugin_slug( $basename ),
3286 );
3287 }
3288
3289 return $active;
3290 }
3291
3292 /**
3293 * Get collection of all plugins with their activation status for a specified blog.
3294 *
3295 * @author Vova Feldman (@svovaf)
3296 * @since 1.1.8
3297 *
3298 * @param int $blog_id Since 2.0.0
3299 *
3300 * @return array Key is the plugin file path and the value is an array of the plugin data.
3301 */
3302 private static function get_all_plugins( $blog_id = 0 ) {
3303 self::require_plugin_essentials();
3304
3305 $all_plugins = fs_get_plugins();
3306
3307 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3308
3309 foreach ( $all_plugins as $basename => &$data ) {
3310 // By default set to inactive (next foreach update the active plugins).
3311 $data['is_active'] = false;
3312 // Enrich with plugin slug.
3313 $data['slug'] = self::get_plugin_slug( $basename );
3314 }
3315
3316 // Flag active plugins.
3317 foreach ( $active_plugins_basenames as $basename ) {
3318 if ( isset( $all_plugins[ $basename ] ) ) {
3319 $all_plugins[ $basename ]['is_active'] = true;
3320 }
3321 }
3322
3323 return $all_plugins;
3324 }
3325
3326 /**
3327 * Get collection of all plugins and if they are network level activated.
3328 *
3329 * @author Vova Feldman (@svovaf)
3330 * @since 2.0.0
3331 *
3332 * @return array Key is the plugin basename and the value is an array of the plugin data.
3333 */
3334 private static function get_network_plugins() {
3335 self::require_plugin_essentials();
3336
3337 $all_plugins = fs_get_plugins();
3338
3339 $network_active_basenames = is_multisite() ?
3340 get_site_option( 'active_sitewide_plugins' ) :
3341 array();
3342
3343 foreach ( $all_plugins as $basename => &$data ) {
3344 // By default set to inactive (next foreach update the active plugins).
3345 $data['is_active'] = false;
3346 // Enrich with plugin slug.
3347 $data['slug'] = self::get_plugin_slug( $basename );
3348 }
3349
3350 // Flag active plugins.
3351 foreach ( $network_active_basenames as $basename ) {
3352 if ( isset( $all_plugins[ $basename ] ) ) {
3353 $all_plugins[ $basename ]['is_active'] = true;
3354 }
3355 }
3356
3357 return $all_plugins;
3358 }
3359
3360 /**
3361 * Cached result of get_site_transient( 'update_plugins' )
3362 *
3363 * @author Vova Feldman (@svovaf)
3364 * @since 1.1.8
3365 *
3366 * @var object
3367 */
3368 private static $_plugins_info;
3369
3370 /**
3371 * Helper function to get specified plugin's slug.
3372 *
3373 * @author Vova Feldman (@svovaf)
3374 * @since 1.1.8
3375 *
3376 * @param $basename
3377 *
3378 * @return string
3379 */
3380 private static function get_plugin_slug( $basename ) {
3381 if ( ! isset( self::$_plugins_info ) ) {
3382 self::$_plugins_info = get_site_transient( 'update_plugins' );
3383 }
3384
3385 $slug = '';
3386
3387 if ( is_object( self::$_plugins_info ) ) {
3388 if ( isset( self::$_plugins_info->no_update ) &&
3389 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3390 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3391 ) {
3392 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3393 } else if ( isset( self::$_plugins_info->response ) &&
3394 isset( self::$_plugins_info->response[ $basename ] ) &&
3395 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3396 ) {
3397 $slug = self::$_plugins_info->response[ $basename ]->slug;
3398 }
3399 }
3400
3401 if ( empty( $slug ) ) {
3402 // Try to find slug from FS data.
3403 $slug = self::find_slug_by_basename( $basename );
3404 }
3405
3406 if ( empty( $slug ) ) {
3407 // Fallback to plugin's folder name.
3408 $slug = dirname( $basename );
3409 }
3410
3411 return $slug;
3412 }
3413
3414 private static $_statics_loaded = false;
3415
3416 /**
3417 * Load static resources.
3418 *
3419 * @author Vova Feldman (@svovaf)
3420 * @since 1.0.1
3421 */
3422 private static function _load_required_static() {
3423 if ( self::$_statics_loaded ) {
3424 return;
3425 }
3426
3427 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3428
3429 self::$_static_logger->entrance();
3430
3431 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3432
3433 if ( is_multisite() ) {
3434 $has_skipped_migration = (
3435 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3436 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3437 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3438 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3439 );
3440
3441 /**
3442 * If the file_slug_map exists on the site level but doesn't exist on the
3443 * network level storage, it means that we need to process the storage with migration.
3444 *
3445 * 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.
3446 *
3447 * @author Vova Feldman (@svovaf)
3448 * @since 2.0.0
3449 */
3450 if (
3451 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3452 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3453 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3454 ) {
3455 self::migrate_options_to_network();
3456 }
3457 }
3458
3459 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3460
3461 FS_DebugManager::load_required_static();
3462
3463 if ( 0 == did_action( 'plugins_loaded' ) ) {
3464 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3465 }
3466
3467 $clone_manager = FS_Clone_Manager::instance();
3468 add_action( 'init', array( $clone_manager, '_init' ) );
3469
3470 add_action( 'admin_footer', array( 'Freemius', '_handle_submenu_external_link' ) );
3471
3472 if ( self::is_plugins_page() || self::is_themes_page() ) {
3473 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3474
3475 /**
3476 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3477 * page.
3478 *
3479 * @author Leo Fajardo (@leorw)
3480 * @since 2.3.0
3481 */
3482 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3483 }
3484
3485 self::$_statics_loaded = true;
3486 }
3487
3488 public static function get_static_logger() {
3489 return self::$_static_logger;
3490 }
3491
3492 public static function get_accounts() {
3493 return self::$_accounts;
3494 }
3495
3496 #--------------------------------------------------------------------------------
3497 #region Clone
3498 #--------------------------------------------------------------------------------
3499
3500 /**
3501 * @author Leo Fajardo (@leorw)
3502 * @since 2.5.0
3503 *
3504 * @param bool $only_if_manual_resolution_is_not_hidden
3505 *
3506 * @return bool
3507 */
3508 private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3509 if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) {
3510 return false;
3511 }
3512
3513 return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired();
3514 }
3515
3516 /**
3517 * @author Leo Fajardo (@leorw)
3518 * @since 2.5.0
3519 *
3520 * @param bool $only_if_manual_resolution_is_not_hidden
3521 */
3522 function is_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3523 if ( ! is_object( $this->_site ) ) {
3524 return false;
3525 }
3526
3527 $blog_id = null;
3528
3529 if (
3530 fs_is_network_admin() &&
3531 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
3532 ) {
3533 // Ensure that we're comparing the network install's URL with the relevant subsite's URL.
3534 $blog_id = $this->_storage->network_install_blog_id;
3535 }
3536
3537 $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
3538
3539 if ( ! $this->_site->is_clone( $site_url ) ) {
3540 return false;
3541 }
3542
3543 return (
3544 ! $only_if_manual_resolution_is_not_hidden ||
3545 ! FS_Clone_Manager::instance()->should_hide_manual_resolution()
3546 );
3547 }
3548
3549 /**
3550 * @author Leo Fajardo (@leorw)
3551 * @since 2.5.0
3552 *
3553 * @param int|null $blog_id
3554 * @param bool $strip_protocol
3555 * @param bool $add_trailing_slash
3556 *
3557 * @return string
3558 */
3559 static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) {
3560 $url = ( ! is_multisite() && defined( 'WP_SITEURL' ) ) ? WP_SITEURL : self::get_site_url_from_wp_option( $blog_id );
3561
3562 if ( $strip_protocol ) {
3563 $url = fs_strip_url_protocol( $url );
3564 }
3565
3566 if ( $add_trailing_slash ) {
3567 $url = trailingslashit( $url );
3568 }
3569
3570 return $url;
3571 }
3572
3573 /**
3574 * @author Leo Fajardo (@leorw)
3575 * @since 2.6.0
3576 *
3577 * @param int|null $blog_id
3578 *
3579 * @return string
3580 */
3581 private static function get_site_url_from_wp_option( $blog_id = null ) {
3582 global $wp_filter;
3583
3584 $site_url_filters = array(
3585 'site_url' => null,
3586 'pre_option_siteurl' => null,
3587 'default_option_siteurl' => null,
3588 'option_siteurl' => null,
3589 );
3590
3591 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3592 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3593 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3594 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3595 unset( $wp_filter[ $hook_name ] );
3596 }
3597 }
3598
3599 $url = get_site_url( $blog_id );
3600
3601 // Re-attach the filters back.
3602 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3603 if ( ! empty( $site_url_filter ) ) {
3604 $wp_filter[ $hook_name ] = $site_url_filter;
3605 }
3606 }
3607
3608 return $url;
3609 }
3610
3611 /**
3612 * @author Leo Fajardo (@leorw)
3613 * @since 2.5.0
3614 *
3615 * @param number $site_id
3616 */
3617 function fetch_install_by_id( $site_id ) {
3618 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3619 }
3620
3621 /**
3622 * @author Leo Fajardo (@leorw)
3623 * @since 2.5.0
3624 *
3625 * @return string|object|bool
3626 */
3627 function _handle_long_term_duplicate() {
3628 $this->_logger->entrance();
3629
3630 $this->delete_current_install( false );
3631
3632 $license = null;
3633
3634 if (
3635 is_object( $this->_license ) &&
3636 ! $this->_license->is_utilized(
3637 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3638 )
3639 ) {
3640 $license = $this->_license;
3641 }
3642
3643 return $this->opt_in(
3644 false,
3645 false,
3646 false,
3647 ( is_object( $license ) ? $license->secret_key : false ),
3648 false,
3649 false,
3650 false,
3651 null,
3652 array(),
3653 false,
3654 ( is_object( $license ) ? $license->user_id : null )
3655 );
3656 }
3657
3658 #endregion
3659
3660 /**
3661 * @author Leo Fajardo (@leorw)
3662 *
3663 * @since 2.1.3
3664 */
3665 public static function migrate_options_to_network() {
3666 self::migrate_accounts_to_network();
3667
3668 // Migrate API options from site level to network level.
3669 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3670 $api_network_options->migrate_to_network();
3671
3672 // Migrate API cache to network level storage.
3673 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3674
3675 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3676 }
3677
3678 #----------------------------------------------------------------------------------
3679 #region Localization
3680 #----------------------------------------------------------------------------------
3681
3682 /**
3683 * Load framework's text domain.
3684 *
3685 * @author Vova Feldman (@svovaf)
3686 * @since 1.2.1
3687 */
3688 static function _load_textdomain() {
3689 if ( ! is_admin() ) {
3690 return;
3691 }
3692
3693 global $fs_active_plugins;
3694
3695 // Works both for plugins and themes.
3696 load_plugin_textdomain(
3697 'freemius',
3698 false,
3699 $fs_active_plugins->newest->sdk_path . '/languages/'
3700 );
3701 }
3702
3703 #endregion
3704
3705 #----------------------------------------------------------------------------------
3706 #region Connectivity Issues
3707 #----------------------------------------------------------------------------------
3708
3709 /**
3710 * Check if Freemius should be turned on for the current plugin install.
3711 *
3712 * Note:
3713 * $this->_is_on is updated in has_api_connectivity()
3714 *
3715 * @author Vova Feldman (@svovaf)
3716 * @since 1.0.9
3717 *
3718 * @return bool
3719 */
3720 function is_on() {
3721 self::$_static_logger->entrance();
3722
3723 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
3724 return false;
3725 }
3726
3727 if ( isset( $this->_is_on ) ) {
3728 return $this->_is_on;
3729 }
3730
3731 // If already installed or pending then sure it's on :)
3732 if ( $this->is_registered() || $this->is_pending_activation() ) {
3733 $this->_is_on = true;
3734
3735 return true;
3736 }
3737
3738 return false;
3739 }
3740
3741 /**
3742 * @author Vova Feldman (@svovaf)
3743 * @since 1.1.7.3
3744 *
3745 * @param bool $flush_if_no_connectivity
3746 *
3747 * @return bool
3748 */
3749 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3750 if ( ! isset( $this->_storage->connectivity_test ) ) {
3751 // Connectivity test was never executed, or cache was cleared.
3752 return true;
3753 }
3754
3755 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3756 if ( WP_FS__IS_HTTP_REQUEST ) {
3757 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3758 // Domain changed.
3759 return true;
3760 }
3761
3762 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3763 // Server IP changed.
3764 return true;
3765 }
3766 }
3767 }
3768
3769 if ( $this->_storage->connectivity_test['is_connected'] &&
3770 $this->_storage->connectivity_test['is_active']
3771 ) {
3772 // API connected and Freemius is active - no need to run connectivity check.
3773 return false;
3774 }
3775
3776 if ( $flush_if_no_connectivity ) {
3777 /**
3778 * If explicitly asked to flush when no connectivity - do it only
3779 * if at least 10 sec passed from the last API connectivity test.
3780 */
3781 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3782 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3783 }
3784
3785 /**
3786 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3787 */
3788 $version = $this->get_plugin_version();
3789 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3790 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3791 return true;
3792 }
3793
3794 return false;
3795 }
3796
3797 /**
3798 * @author Leo Fajardo (@leorw)
3799 * @since 2.5.4
3800 *
3801 * @param bool $is_update
3802 *
3803 * @return bool
3804 */
3805 private function should_turn_fs_on( $is_update = true ) {
3806 if (
3807 empty( $this->_plugin->opt_in_moderation ) ||
3808 ! is_array( $this->_plugin->opt_in_moderation )
3809 ) {
3810 return true;
3811 }
3812
3813 $optin_config = $this->_plugin->opt_in_moderation;
3814
3815 if (
3816 WP_FS__IS_LOCALHOST &&
3817 ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] )
3818 ) {
3819 return true;
3820 }
3821
3822 $optin_config_key = $is_update ?
3823 'updates' :
3824 'new';
3825
3826 if ( ! isset( $optin_config[ $optin_config_key ] ) ) {
3827 return true;
3828 }
3829
3830 $visibility_percentage = $optin_config[ $optin_config_key ];
3831
3832 if ( 0 == $visibility_percentage ) {
3833 return false;
3834 }
3835
3836 if ( ! is_numeric( $visibility_percentage ) ) {
3837 return true;
3838 }
3839
3840 $min = 1;
3841 $max = 100;
3842
3843 if ( function_exists( 'random_int' ) ) {
3844 $random = random_int( $min, $max ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound
3845 } else {
3846 $random = rand( $min, $max );
3847 }
3848
3849 return ( $random <= $visibility_percentage );
3850 }
3851
3852 /**
3853 * Check if there's any connectivity issue to Freemius API.
3854 *
3855 * @author Vova Feldman (@svovaf)
3856 * @since 1.0.9
3857 *
3858 * @param bool $flush_if_no_connectivity
3859 *
3860 * @return bool|null
3861 */
3862 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3863 $this->_logger->entrance();
3864
3865 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3866 return $this->_has_api_connection;
3867 }
3868
3869 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3870 isset( $this->_storage->connectivity_test ) &&
3871 true === $this->_storage->connectivity_test['is_connected']
3872 ) {
3873 $this->clear_connectivity_info();
3874 }
3875
3876 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3877 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3878 /**
3879 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3880 *
3881 * @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.
3882 */
3883 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3884 $this->is_premium() ||
3885 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3886
3887 return $this->_has_api_connection;
3888 }
3889
3890 if (
3891 ! empty( $this->_storage->connectivity_test ) &&
3892 isset( $this->_storage->connectivity_test['is_active'] )
3893 ) {
3894 $is_connected = isset( $this->_storage->connectivity_test['is_connected'] ) ?
3895 $this->_storage->connectivity_test['is_connected'] :
3896 null;
3897 $is_active = ( $this->_storage->connectivity_test['is_active'] || is_object( $this->_site ) );
3898 } else {
3899 $is_connected = null;
3900 $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) );
3901 }
3902
3903 $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), $is_connected );
3904
3905 if ( $is_active ) {
3906 $this->_is_on = true;
3907 }
3908
3909 return $this->_has_api_connection;
3910 }
3911
3912 /**
3913 * @author Leo Fajardo (@leorw)
3914 * @since 2.5.4
3915 */
3916 private function clear_connectivity_info() {
3917 unset( $this->_storage->connectivity_test );
3918
3919 FS_Api::clear_force_http_flag();
3920 }
3921
3922 /**
3923 * @author Vova Feldman (@svovaf)
3924 * @since 1.1.7.4
3925 *
3926 * @param object $pong
3927 * @param bool|null $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 ( false === $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 * @author Leo Fajardo (@leorw)
3963 * @since 2.5.4
3964 *
3965 * @param bool $is_connected
3966 */
3967 private function update_connectivity_info( $is_connected ) {
3968 $this->store_connectivity_info(
3969 // This is true since we update the connection info only after a successful opt-in or license activation which means that Freemius has already been on even before the process.
3970 (object) array( 'is_active' => true ),
3971 $is_connected
3972 );
3973 }
3974
3975 /**
3976 * Force turning Freemius on.
3977 *
3978 * @author Vova Feldman (@svovaf)
3979 * @since 1.1.8.1
3980 *
3981 * @return bool TRUE if successfully turned on.
3982 */
3983 private function turn_on() {
3984 $this->_logger->entrance();
3985
3986 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3987 return false;
3988 }
3989
3990 $updated_connectivity = $this->_storage->connectivity_test;
3991 $updated_connectivity['is_active'] = true;
3992 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3993 $this->_storage->connectivity_test = $updated_connectivity;
3994
3995 $this->_is_on = true;
3996
3997 return true;
3998 }
3999
4000 /**
4001 * Anonymous and unique site identifier (Hash).
4002 *
4003 * @author Vova Feldman (@svovaf)
4004 * @since 1.1.0
4005 *
4006 * @param null|int $blog_id Since 2.0.0
4007 *
4008 * @return string
4009 */
4010 function get_anonymous_id( $blog_id = null ) {
4011 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4012
4013 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4014 $key = self::get_unfiltered_site_url( $blog_id, true );
4015
4016 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4017 if ( empty( $secure_auth ) ||
4018 false !== strpos( $secure_auth, ' ' ) ||
4019 'put your unique phrase here' === $secure_auth
4020 ) {
4021 // Protect against default auth key.
4022 $secure_auth = md5( microtime() );
4023 }
4024
4025 /**
4026 * Base the unique identifier on the WP secure authentication key. Which
4027 * turns the key into a secret anonymous identifier. This will help us
4028 * to avoid duplicate installs generation on the backend upon opt-in.
4029 *
4030 * @author Vova Feldman (@svovaf)
4031 * @since 1.2.3
4032 */
4033 $unique_id = md5( $key . $secure_auth );
4034
4035 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4036 }
4037
4038 $this->_logger->departure( $unique_id );
4039
4040 return $unique_id;
4041 }
4042
4043 /**
4044 * Returns anonymous network ID.
4045 *
4046 * @since 2.4.3
4047 *
4048 * @return string
4049 */
4050 function get_anonymous_network_id() {
4051 return $this->get_anonymous_id( get_network()->site_id );
4052 }
4053
4054 /**
4055 * @author Vova Feldman (@svovaf)
4056 * @since 1.1.7.4
4057 *
4058 * @return \WP_User
4059 */
4060 static function _get_current_wp_user() {
4061 self::require_pluggable_essentials();
4062 self::wp_cookie_constants();
4063
4064 return wp_get_current_user();
4065 }
4066
4067 /**
4068 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4069 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4070 * is network activated the cookie constants are only configured after the network
4071 * plugins activation, therefore, if we don't define those constants WP will throw
4072 * PHP warnings/notices.
4073 *
4074 * @author Vova Feldman (@svovaf)
4075 * @since 2.1.1
4076 */
4077 private static function wp_cookie_constants() {
4078 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4079 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4080 ) {
4081 return;
4082 }
4083
4084 /**
4085 * Used to guarantee unique hash cookies
4086 *
4087 * @since 1.5.0
4088 */
4089 if ( ! defined( 'COOKIEHASH' ) ) {
4090 $siteurl = get_site_option( 'siteurl' );
4091 if ( $siteurl ) {
4092 define( 'COOKIEHASH', md5( $siteurl ) );
4093 } else {
4094 define( 'COOKIEHASH', '' );
4095 }
4096 }
4097
4098 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4099 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4100 }
4101
4102 /**
4103 * @since 2.5.0
4104 */
4105 if ( ! defined( 'AUTH_COOKIE' ) ) {
4106 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4107 }
4108
4109 /**
4110 * @since 2.6.0
4111 */
4112 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4113 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4114 }
4115 }
4116
4117 /**
4118 * @author Vova Feldman (@svovaf)
4119 * @since 2.1.0
4120 *
4121 * @return int
4122 */
4123 static function get_current_wp_user_id() {
4124 $wp_user = self::_get_current_wp_user();
4125
4126 return $wp_user->ID;
4127 }
4128
4129 /**
4130 * @author Vova Feldman (@svovaf)
4131 * @since 1.2.1.7
4132 *
4133 * @param string $email
4134 *
4135 * @return bool
4136 */
4137 static function is_valid_email( $email ) {
4138 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4139 return false;
4140 }
4141
4142 $parts = explode( '@', $email );
4143
4144 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4145 return false;
4146 }
4147
4148 $blacklist = array(
4149 'admin.',
4150 'webmaster.',
4151 'localhost.',
4152 'dev.',
4153 'development.',
4154 'test.',
4155 'stage.',
4156 'staging.',
4157 );
4158
4159 // Make sure domain is not one of the blacklisted.
4160 foreach ( $blacklist as $invalid ) {
4161 if ( 0 === strpos( $parts[1], $invalid ) ) {
4162 return false;
4163 }
4164 }
4165
4166 // Get the UTF encoded domain name.
4167 /**
4168 * @note - The check of `defined('...')` is there to account for PHP servers compiled with some older version of ICU where the constants are not defined.
4169 * @author - @swashata
4170 */
4171 $is_new_idn_available = (
4172 version_compare( PHP_VERSION, '5.6.40') > 0 &&
4173 defined( 'IDNA_DEFAULT' ) &&
4174 defined( 'INTL_IDNA_VARIANT_UTS46' )
4175 );
4176 if ( $is_new_idn_available ) {
4177 $domain = idn_to_ascii( $parts[1], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 );
4178 } else {
4179 $domain = idn_to_ascii( $parts[1] ); // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
4180 }
4181
4182 $domain = $domain . '.';
4183
4184 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4185 }
4186
4187 #endregion
4188
4189 #----------------------------------------------------------------------------------
4190 #region Email
4191 #----------------------------------------------------------------------------------
4192
4193 /**
4194 * Generates and sends an HTML email with customizable sections.
4195 *
4196 * @author Leo Fajardo (@leorw)
4197 * @since 1.1.2
4198 *
4199 * @param string $to_address
4200 * @param string $subject
4201 * @param array $sections
4202 * @param array $headers
4203 *
4204 * @return bool Whether the email contents were sent successfully.
4205 */
4206 private function send_email(
4207 $to_address,
4208 $subject,
4209 $sections = array(),
4210 $headers = array()
4211 ) {
4212 $default_sections = $this->get_email_sections();
4213
4214 // Insert new sections or replace the default email sections.
4215 if ( is_array( $sections ) && ! empty( $sections ) ) {
4216 foreach ( $sections as $section_id => $custom_section ) {
4217 if ( ! isset( $default_sections[ $section_id ] ) ) {
4218 // If the section does not exist, add it.
4219 $default_sections[ $section_id ] = $custom_section;
4220 } else {
4221 // If the section already exists, override it.
4222 $current_section = $default_sections[ $section_id ];
4223
4224 // Replace the current section's title if a custom section title exists.
4225 if ( isset( $custom_section['title'] ) ) {
4226 $current_section['title'] = $custom_section['title'];
4227 }
4228
4229 // Insert new rows under the current section or replace the default rows.
4230 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4231 foreach ( $custom_section['rows'] as $row_id => $row ) {
4232 $current_section['rows'][ $row_id ] = $row;
4233 }
4234 }
4235
4236 $default_sections[ $section_id ] = $current_section;
4237 }
4238 }
4239 }
4240
4241 $vars = array( 'sections' => $default_sections );
4242 $message = fs_get_template( 'email.php', $vars );
4243
4244 // Set the type of email to HTML.
4245 $headers[] = 'Content-type: text/html; charset=UTF-8';
4246
4247 $header_string = implode( "\r\n", $headers );
4248
4249 return wp_mail(
4250 $to_address,
4251 $subject,
4252 $message,
4253 $header_string
4254 );
4255 }
4256
4257 /**
4258 * Generates the data for the sections of the email content.
4259 *
4260 * @author Leo Fajardo (@leorw)
4261 * @since 1.1.2
4262 *
4263 * @return array
4264 */
4265 private function get_email_sections() {
4266 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4267 $current_user = self::_get_current_wp_user();
4268
4269 // Retrieve the cURL version information so that we can get the version number below.
4270 $curl_version_information = curl_version();
4271
4272 $active_plugin = self::get_active_plugins();
4273
4274 // Generate the list of active plugins separated by new line.
4275 $active_plugin_string = '';
4276 foreach ( $active_plugin as $plugin ) {
4277 $active_plugin_string .= sprintf(
4278 '<a href="%s">%s</a> [v%s]<br>',
4279 $plugin['PluginURI'],
4280 $plugin['Name'],
4281 $plugin['Version']
4282 );
4283 }
4284
4285 $server_ip = WP_FS__REMOTE_ADDR;
4286
4287 // Add PHP info for deeper investigation.
4288 ob_start();
4289 phpinfo();
4290 $php_info = ob_get_clean();
4291
4292 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4293
4294 // Generate the default email sections.
4295 $sections = array(
4296 'sdk' => array(
4297 'title' => 'SDK',
4298 'rows' => array(
4299 'fs_version' => array( 'FS Version', $this->version ),
4300 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4301 )
4302 ),
4303 'plugin' => array(
4304 'title' => ucfirst( $this->get_module_type() ),
4305 'rows' => array(
4306 'name' => array( 'Name', $this->get_plugin_name() ),
4307 'version' => array( 'Version', $this->get_plugin_version() )
4308 )
4309 ),
4310 'api' => array(
4311 'title' => 'API Subdomain',
4312 'rows' => array(
4313 'dns' => array(
4314 'DNS_CNAME',
4315 function_exists( 'dns_get_record' ) ?
4316 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4317 'dns_get_record() disabled/blocked'
4318 ),
4319 'ip' => array(
4320 'IP',
4321 function_exists( 'gethostbyname' ) ?
4322 gethostbyname( $api_domain ) :
4323 'gethostbyname() disabled/blocked'
4324 ),
4325 ),
4326 ),
4327 'site' => array(
4328 'title' => 'Site',
4329 'rows' => array(
4330 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4331 'address' => array( 'Address', site_url() ),
4332 'host' => array(
4333 'HTTP_HOST',
4334 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4335 ),
4336 'hosting' => array(
4337 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4338 fs_request_get( 'hosting_company' ) :
4339 'Unknown',
4340 ),
4341 'server_addr' => array(
4342 'SERVER_ADDR',
4343 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4344 )
4345 )
4346 ),
4347 'user' => array(
4348 'title' => 'User',
4349 'rows' => array(
4350 'email' => array( 'Email', $current_user->user_email ),
4351 'first' => array( 'First', $current_user->user_firstname ),
4352 'last' => array( 'Last', $current_user->user_lastname )
4353 )
4354 ),
4355 'plugins' => array(
4356 'title' => 'Plugins',
4357 'rows' => array(
4358 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4359 )
4360 ),
4361 'php_info' => array(
4362 'title' => 'PHP Info',
4363 'rows' => array(
4364 'info' => array( $php_info )
4365 ),
4366 )
4367 );
4368
4369 // Allow the sections to be modified by other code.
4370 $sections = $this->apply_filters( 'email_template_sections', $sections );
4371
4372 return $sections;
4373 }
4374
4375 #endregion
4376
4377 #----------------------------------------------------------------------------------
4378 #region Initialization
4379 #----------------------------------------------------------------------------------
4380
4381 /**
4382 * Init plugin's Freemius instance.
4383 *
4384 * @author Vova Feldman (@svovaf)
4385 * @since 1.0.1
4386 *
4387 * @param number $id
4388 * @param string $public_key
4389 * @param bool $is_live
4390 * @param bool $is_premium
4391 */
4392 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4393 $this->_logger->entrance();
4394
4395 $this->dynamic_init( array(
4396 'id' => $id,
4397 'public_key' => $public_key,
4398 'is_live' => $is_live,
4399 'is_premium' => $is_premium,
4400 ) );
4401 }
4402
4403 /**
4404 * Dynamic initiator, originally created to support initiation
4405 * with parent_id for add-ons.
4406 *
4407 * @author Vova Feldman (@svovaf)
4408 * @since 1.0.6
4409 *
4410 * @param array $plugin_info
4411 *
4412 * @throws Freemius_Exception
4413 */
4414 function dynamic_init( array $plugin_info ) {
4415 $this->_logger->entrance();
4416
4417 $this->parse_settings( $plugin_info );
4418
4419 $this->register_after_settings_parse_hooks();
4420
4421 /**
4422 * If anonymous but there's already a user entity and the user's site is associated with a valid license or trial period, update the anonymous mode accordingly.
4423 *
4424 * @todo Remove this entire `if` block after several releases as starting from this version, the anonymous mode will already be updated accordingly after a purchase.
4425 */
4426 if ( $this->is_anonymous() ) {
4427 $is_network_level = ( $this->_is_network_active && fs_is_network_admin() );
4428
4429 if (
4430 ! $is_network_level ||
4431 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
4432 ) {
4433 if ( $this->is_paying_or_trial() ) {
4434 $this->reset_anonymous_mode( $is_network_level );
4435 }
4436 } else {
4437 $network = get_network();
4438
4439 if ( is_object( $network ) ) {
4440 $main_blog_id = $network->site_id;
4441 $first_install = $this->get_install_by_blog_id( $main_blog_id );
4442
4443 if ( is_object( $first_install ) ) {
4444 $this->_storage->network_install_blog_id = $main_blog_id;
4445 $this->_storage->network_user_id = $first_install->user_id;
4446 }
4447 }
4448 }
4449 }
4450
4451 if ( $this->should_stop_execution() ) {
4452 return;
4453 }
4454
4455 if ( ! $this->is_registered() ) {
4456 if ( $this->is_anonymous() ) {
4457 // If user skipped, no need to test connectivity.
4458 $this->_has_api_connection = true;
4459 $this->_is_on = true;
4460 } else {
4461 if ( false === $this->has_api_connectivity() ) {
4462 return;
4463 } else {
4464 if ( $this->_anonymous_mode ) {
4465 // Simulate anonymous mode.
4466 $this->_is_anonymous = true;
4467 }
4468 }
4469 }
4470 }
4471
4472 /**
4473 * This should be executed even if Freemius is off for the core module,
4474 * otherwise, the add-ons dialog box won't work properly. This is especially
4475 * relevant when the developer decided to turn FS off for existing users.
4476 *
4477 * @author Vova Feldman (@svovaf)
4478 */
4479 if ( $this->is_user_in_admin() &&
4480 'plugin-information' === fs_request_get( 'tab', false ) &&
4481 $this->should_use_freemius_updater_and_dialog() &&
4482 (
4483 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4484 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4485 )
4486 ) {
4487 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4488
4489 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4490 }
4491
4492 // Check if Freemius is on for the current plugin.
4493 // This MUST be executed after all the plugin variables has been loaded.
4494 if ( ! $this->is_registered() && ! $this->is_on() ) {
4495 return;
4496 }
4497
4498 $this->hook_callback_to_sync_cron();
4499
4500 if ( $this->has_api_connectivity() && ! self::is_cron() && $this->is_user_in_admin() ) {
4501 /**
4502 * Schedule daily data sync cron if:
4503 *
4504 * 1. User opted-in (for tracking).
4505 * 2. If skipped, but later upgraded (opted-in via upgrade).
4506 *
4507 * @author Vova Feldman (@svovaf)
4508 * @since 1.1.7.3
4509 *
4510 */
4511 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
4512 $this->maybe_schedule_sync_cron();
4513 }
4514
4515 /**
4516 * Check if requested for manual blocking background sync.
4517 */
4518 if ( fs_request_has( 'background_sync' ) ) {
4519 self::require_pluggable_essentials();
4520 self::wp_cookie_constants();
4521
4522 $this->run_manual_sync();
4523 }
4524 }
4525
4526 if ( $this->is_registered() ) {
4527 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
4528
4529 $this->hook_callback_to_install_sync();
4530 }
4531
4532 if ( $this->is_addon() ) {
4533 if ( $this->is_parent_plugin_installed() ) {
4534 // Link to parent FS.
4535 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4536
4537 // Get parent plugin reference.
4538 $this->_parent_plugin = $this->_parent->get_plugin();
4539 }
4540 }
4541
4542 if ( $this->is_user_in_admin() ) {
4543 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
4544 $this->_admin_notices->add_sticky(
4545 sprintf(
4546 /* translators: %s: License type (e.g. you have a professional license) */
4547 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
4548 fs_request_get( 'purchased_plan' )
4549 ) .
4550 sprintf(
4551 $this->get_text_inline(" The %s's %sdownload link%s, license key, and installation instructions have been sent to %s. If you can't find the email after 5 min, please check your spam box.", 'post-purchase-email-sent-message' ),
4552 $this->get_module_label( true ),
4553 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
4554 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
4555 sprintf(
4556 '<strong>%s</strong>',
4557 fs_request_get( 'purchase_email' )
4558 )
4559 ),
4560 'plan_purchased',
4561 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4562 );
4563 }
4564
4565 if ( $this->is_addon() ) {
4566 if ( ! $this->is_parent_plugin_installed() ) {
4567 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4568
4569 if ( isset( $plugin_info['parent'] ) ) {
4570 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4571 }
4572
4573 $this->_admin_notices->add(
4574 ( ! empty( $parent_name ) ?
4575 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 ) :
4576 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() )
4577 ),
4578 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4579 'error'
4580 );
4581
4582 return;
4583 } else {
4584 $is_network_admin = fs_is_network_admin();
4585
4586 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4587 // If add-on activated and parent not, automatically install parent for the user.
4588 $this->activate_parent_account( $this->_parent );
4589 } else if (
4590 $this->_parent->is_registered() &&
4591 ! $this->is_registered() &&
4592 /**
4593 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4594 * * Network active and in network admin - network activate add-on account.
4595 * * Network active and not in network admin - activate add-on account for the current blog.
4596 * * Not network active and not in network admin - activate add-on account for the current blog.
4597 *
4598 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4599 *
4600 * @author Leo Fajardo (@leorw)
4601 * @since 2.3.0
4602 */
4603 ( $this->is_network_active() || ! $is_network_admin )
4604 ) {
4605 $premium_license = null;
4606
4607 if (
4608 ! $this->has_free_plan() &&
4609 $this->is_bundle_license_auto_activation_enabled() &&
4610 $this->_parent->is_activated_with_bundle_license()
4611 ) {
4612 /**
4613 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
4614 *
4615 * @author Leo Fajardo (@leorw)
4616 * @since 2.4.0
4617 */
4618 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
4619
4620 if (
4621 is_object( $bundle_license ) &&
4622 ! empty( $bundle_license->products ) &&
4623 in_array( $this->get_id(), $bundle_license->products )
4624 ) {
4625 $premium_license = $bundle_license;
4626 }
4627 }
4628
4629 if ( $this->has_free_plan() || is_object( $premium_license) ) {
4630 // If parent plugin activated, automatically install add-on for the user.
4631 $this->_activate_addon_account(
4632 $this->_parent,
4633 ( $this->is_network_active() && $is_network_admin ) ?
4634 true :
4635 get_current_blog_id(),
4636 $premium_license
4637 );
4638 }
4639 }
4640
4641 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4642 if ( $this->is_premium() ) {
4643 // Remove add-on download admin-notice.
4644 $this->_parent->_admin_notices->remove_sticky( array(
4645 'addon_plan_upgraded_' . $this->_slug,
4646 'no_addon_license_' . $this->_slug,
4647 ) );
4648 }
4649
4650 // $this->deactivate_premium_only_addon_without_license();
4651 }
4652 }
4653
4654 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4655
4656 // if ( $this->is_registered() ||
4657 // $this->is_anonymous() ||
4658 // $this->is_pending_activation()
4659 // ) {
4660 // $this->_init_admin();
4661 // }
4662 }
4663
4664 /**
4665 * Should be called outside `$this->is_user_in_admin()` scope
4666 * because the updater has some logic that needs to be executed
4667 * during AJAX calls.
4668 *
4669 * Currently, we need to hook to the `http_request_host_is_external` filter.
4670 * In the future, there might be additional logic added.
4671 *
4672 * @author Vova Feldman
4673 * @since 1.2.1.6
4674 */
4675 if (
4676 $this->should_use_freemius_updater_and_dialog() &&
4677 (
4678 $this->is_premium() ||
4679 /**
4680 * If not premium but the premium version is installed, also instantiate the updater so that the
4681 * plugin information dialog of the premium version will have the information from the server.
4682 *
4683 * @author Leo Fajardo (@leorw)
4684 * @since 2.2.3
4685 */
4686 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4687 ) &&
4688 $this->has_release_on_freemius() &&
4689 ( ! $this->is_unresolved_clone( true ) )
4690 ) {
4691 FS_Plugin_Updater::instance( $this );
4692 }
4693
4694 $this->do_action( 'initiated' );
4695
4696 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4697 if ( isset( $this->_storage->prev_is_premium ) ) {
4698 $this->apply_filters(
4699 'after_code_type_change',
4700 // New code type.
4701 $this->_plugin->is_premium
4702 );
4703 } else {
4704 // Set for code type for the first time.
4705 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4706 }
4707 }
4708
4709 if ( ! $this->is_addon() ) {
4710 if ( $this->is_registered() ) {
4711 // Fix for upgrade from versions < 1.0.9.
4712 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4713 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4714 }
4715
4716 $this->do_action( 'after_init_plugin_registered' );
4717 } else if ( $this->is_anonymous() ) {
4718 $this->do_action( 'after_init_plugin_anonymous' );
4719 } else if ( $this->is_pending_activation() ) {
4720 $this->do_action( 'after_init_plugin_pending_activations' );
4721 }
4722 } else {
4723 if ( $this->is_registered() ) {
4724 $this->do_action( 'after_init_addon_registered' );
4725 } else if ( $this->is_anonymous() ) {
4726 $this->do_action( 'after_init_addon_anonymous' );
4727 } else if ( $this->is_pending_activation() ) {
4728 $this->do_action( 'after_init_addon_pending_activations' );
4729 }
4730 }
4731 }
4732
4733 /**
4734 * @author Leo Fajardo (@leorw)
4735 * @since 2.2.3
4736 *
4737 * @return bool
4738 */
4739 private function should_use_freemius_updater_and_dialog() {
4740 return (
4741 /**
4742 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
4743 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
4744 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
4745 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
4746 * plugin details from .org).
4747 */
4748 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
4749 (
4750 ! self::is_plugin_install_page() &&
4751 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
4752 ( 'install-plugin' !== fs_request_get( 'action' ) )
4753 )
4754 );
4755 }
4756
4757 /**
4758 * @param string[] $permissions
4759 * @param bool $is_enabled
4760 * @param int|null $blog_id
4761 *
4762 * @return true|object `true` on success, API error object on failure.
4763 */
4764 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
4765 $this->_logger->entrance();
4766
4767 $params = array(
4768 'permissions' => implode( ',', $permissions ),
4769 'is_enabled' => $is_enabled,
4770 );
4771
4772 $current_blog_id = get_current_blog_id();
4773 $is_blog_switched = false;
4774 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
4775 $is_blog_switched = $this->switch_to_blog( $blog_id );
4776 }
4777
4778 $result = $this->api_site_call( '/permissions.json', 'put', $params );
4779
4780 if ( $is_blog_switched ) {
4781 $this->switch_to_blog( $current_blog_id );
4782 }
4783
4784 if (
4785 ! $this->is_api_result_object( $result ) ||
4786 ! isset( $result->install_id )
4787 ) {
4788 $this->_logger->api_error( $result );
4789
4790 return $result;
4791 }
4792
4793 return true;
4794 }
4795
4796 /**
4797 * @param string[] $permissions
4798 * @param bool $is_enabled
4799 * @param bool $has_site_delegated_connection
4800 *
4801 * @return true|object `true` on success, API error object on failure.
4802 */
4803 private function update_network_permissions(
4804 array $permissions,
4805 $is_enabled,
4806 &$has_site_delegated_connection
4807 ) {
4808 $this->_logger->entrance();
4809
4810 $install_id_2_blog_id = array();
4811 $install_by_blog_id = $this->get_blog_install_map();
4812
4813 $has_site_delegated_connection = false;
4814
4815 foreach ( $install_by_blog_id as $blog_id => $install ) {
4816 if ( $this->is_site_delegated_connection( $blog_id ) ) {
4817 // Only update permissions of non-delegated installs.
4818 $has_site_delegated_connection = true;
4819 continue;
4820 }
4821
4822 $install_id_2_blog_id[ $install->id ] = $blog_id;
4823 }
4824
4825 if ( empty( $install_id_2_blog_id ) ) {
4826 return true;
4827 }
4828
4829 $params = array(
4830 'permissions' => implode( ',', $permissions ),
4831 'is_enabled' => $is_enabled,
4832 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
4833 );
4834
4835 // Send update to FS.
4836 $result = $this->get_current_or_network_user_api_scope()->call(
4837 "/plugins/{$this->_module_id}/installs/permissions.json",
4838 'put',
4839 $params
4840 );
4841
4842 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
4843 $this->_logger->api_error( $result );
4844
4845 return $result;
4846 }
4847
4848 return true;
4849 }
4850
4851 /**
4852 * @param mixed $result
4853 *
4854 * @return string
4855 */
4856 private function get_api_error_message( $result ) {
4857 $error_message = sprintf( $this->get_text_inline( 'There was an unexpected API error while processing your request. Please try again in a few minutes and if it still doesn\'t work, contact the %s\'s author with the following:',
4858 'unexpected-api-error' ), $this->_module_type ) . ' ';
4859
4860 if (
4861 $this->is_api_error( $result ) &&
4862 isset( $result->error )
4863 ) {
4864 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
4865
4866 $error_message .= "<b>{$result->error->message}{$code}</b>";
4867 } else {
4868 $error_message .= var_export( $result, true );
4869 }
4870
4871 return $error_message;
4872 }
4873
4874 /**
4875 * @author Vova Feldman (@svovaf)
4876 * @since 2.5.1
4877 */
4878 function _toggle_permission_tracking_callback() {
4879 $this->_logger->entrance();
4880
4881 $this->check_ajax_referer( 'toggle_permission_tracking' );
4882
4883 if ( ! $this->is_registered( true ) ) {
4884 self::shoot_ajax_failure( 'User never opted-in.' );
4885 }
4886
4887 $is_enabled = fs_request_get_bool( 'is_enabled' );
4888 $permissions = fs_request_get( 'permissions' );
4889
4890 if ( ! is_string( $permissions ) ) {
4891 self::shoot_ajax_failure( 'The permissions param must be a string.' );
4892 }
4893
4894 $permissions = explode( ',', $permissions );
4895
4896 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
4897
4898 if ( true !== $result ) {
4899 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
4900 }
4901
4902 self::shoot_ajax_success();
4903 }
4904
4905 /**
4906 * @param string[] $permissions
4907 * @param bool $is_enabled
4908 * @param int|null $blog_id
4909 *
4910 * @return bool|mixed `true` if updated successfully or no update is needed.
4911 */
4912 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
4913 if ( ! $this->is_registered( true ) ) {
4914 // User never opted-in.
4915 return false;
4916 }
4917
4918 // Check if permissions are already set as needed.
4919 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
4920 /**
4921 * Note:
4922 * When running on the network admin, there's no need to iterate through all the installs individually since network opt-in permissions are managed for ALL non-delegated installs through a single option (per permission) on the network-level storage.
4923 */
4924 return true;
4925 }
4926
4927 $api_managed_permissions = array_intersect(
4928 $permissions,
4929 FS_Permission_Manager::get_api_managed_permission_ids()
4930 );
4931
4932 if (
4933 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
4934 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
4935 ) {
4936 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
4937 }
4938
4939 if ( ! empty( $api_managed_permissions ) ) {
4940 $has_site_delegated_connection = false;
4941
4942 if (
4943 ! $is_enabled &&
4944 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
4945 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
4946 ) {
4947 /**
4948 * If we are turning off a permission and the extensions permission is off too, enrich the permissions update request to also turn off extensions tracking, as currently when opting in with extensions tracking disabled the extensions tracking is off but the API isn't aware of it.
4949 *
4950 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
4951 */
4952 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
4953 }
4954
4955 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
4956 $result = $this->update_network_permissions(
4957 $api_managed_permissions,
4958 $is_enabled,
4959 $has_site_delegated_connection
4960 );
4961 } else {
4962 $result = $this->update_site_permissions(
4963 $api_managed_permissions,
4964 $is_enabled,
4965 $blog_id
4966 );
4967 }
4968
4969 if ( true !== $result ) {
4970 return $result;
4971 }
4972
4973 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
4974 if ( $is_enabled ) {
4975 $this->schedule_sync_cron();
4976 } else {
4977 $this->clear_sync_cron( ! $has_site_delegated_connection );
4978 }
4979 }
4980
4981 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
4982 $this->toggle_user_permission( $is_enabled, $blog_id );
4983 }
4984 }
4985
4986 $this->update_tracking_permissions(
4987 $permissions,
4988 $is_enabled,
4989 $blog_id
4990 );
4991
4992 return true;
4993 }
4994
4995 /**
4996 * @param bool $is_enabled
4997 * @param int|null $blog_id
4998 */
4999 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5000 $network_or_blog_ids = is_numeric( $blog_id ) ?
5001 $blog_id :
5002 fs_is_network_admin();
5003
5004 if ( $is_enabled ) {
5005 $this->reset_anonymous_mode( $network_or_blog_ids );
5006 } else {
5007 $this->skip_connection( $network_or_blog_ids );
5008 }
5009 }
5010
5011 /**
5012 * Opt-in back into usage tracking.
5013 *
5014 * Note: This will only work if the user opted-in previously.
5015 *
5016 * Returns:
5017 * 1. FALSE - If the user never opted-in.
5018 * 2. TRUE - If successfully opted-in back to usage tracking.
5019 * 3. object - API result on failure.
5020 *
5021 * @author Leo Fajardo (@leorw)
5022 * @since 1.2.1.5
5023 *
5024 * @bool $is_enabled
5025 *
5026 * @return bool|object
5027 */
5028 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5029 $this->_logger->entrance();
5030
5031 return $this->toggle_permission_tracking(
5032 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5033 $is_enabled,
5034 $blog_id
5035 );
5036 }
5037
5038 /**
5039 * If user opted-in and later disabled usage-tracking,
5040 * re-allow tracking for licensing and updates.
5041 *
5042 * @author Leo Fajardo (@leorw)
5043 * @since 1.2.1.5
5044 *
5045 * @param bool $is_context_single_site
5046 */
5047 private function reconnect_locally( $is_context_single_site = false ) {
5048 $this->_logger->entrance();
5049
5050 if ( ! $this->is_registered() ) {
5051 return;
5052 }
5053
5054 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5055 if ( $this->is_tracking_prohibited() ) {
5056 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5057 }
5058 } else {
5059 $installs_map = $this->get_blog_install_map();
5060 foreach ( $installs_map as $blog_id => $install ) {
5061 /**
5062 * @var FS_Site $install
5063 */
5064 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5065 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5066 }
5067 }
5068 }
5069 }
5070
5071 /**
5072 * Update permission tracking flags. When updating in a network context, in addition to updating the network-level flags, also update the permissions on the site-level for all non-delegated sites.
5073 *
5074 * @param string[] $permissions
5075 * @param bool $is_enabled
5076 * @param int|null $blog_id
5077 *
5078 * @return array
5079 */
5080 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5081 // Alias.
5082 $permission_manager = FS_Permission_Manager::instance( $this );
5083
5084 $network_or_blog_ids = is_numeric( $blog_id ) ?
5085 $blog_id :
5086 fs_is_network_admin();
5087
5088 if ( true === $network_or_blog_ids ) {
5089 // Update the permission for all non-delegated sub-sites.
5090 $blog_ids = $this->get_non_delegated_blog_ids();
5091
5092 // Add the network-level to the array, to update the permission on the network-level storage.
5093 array_unshift( $blog_ids, null );
5094 }
5095 else
5096 {
5097 if ( false === $network_or_blog_ids ) {
5098 $network_or_blog_ids = null;
5099 }
5100
5101 $blog_ids = is_array( $network_or_blog_ids ) ?
5102 $network_or_blog_ids :
5103 array( $network_or_blog_ids );
5104 }
5105
5106 $result = array();
5107 foreach ( $permissions as $permission ) {
5108 $permission = trim( $permission );
5109 $is_permission_supported = true;
5110
5111 foreach ( $blog_ids as $id ) {
5112 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5113 $permission,
5114 $is_enabled,
5115 $id
5116 );
5117 }
5118
5119 if ( ! $is_permission_supported ) {
5120 $permission = 'no_match';
5121 }
5122
5123 $result[ $permission ] = $is_enabled;
5124 }
5125
5126 return $result;
5127 }
5128
5129 /**
5130 * Parse plugin's settings (as defined by the plugin dev).
5131 *
5132 * @author Vova Feldman (@svovaf)
5133 * @since 1.1.7.3
5134 *
5135 * @param array $plugin_info
5136 *
5137 * @throws \Freemius_Exception
5138 */
5139 private function parse_settings( &$plugin_info ) {
5140 $this->_logger->entrance();
5141
5142 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5143 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5144 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5145 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5146 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5147
5148 /**
5149 * @author Vova Feldman (@svovaf)
5150 * @since 1.1.9 Try to pull secret key from external config.
5151 */
5152 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5153 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5154 }
5155
5156 if ( isset( $plugin_info['parent'] ) ) {
5157 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5158 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5159 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5160 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5161 }
5162
5163 if ( false === $id ) {
5164 throw new Freemius_Exception( array(
5165 'error' => array(
5166 'type' => 'ParameterNotSet',
5167 'message' => 'Plugin id parameter is not set.',
5168 'code' => 'plugin_id_not_set',
5169 'http' => 500,
5170 )
5171 ) );
5172 }
5173 if ( false === $public_key ) {
5174 throw new Freemius_Exception( array(
5175 'error' => array(
5176 'type' => 'ParameterNotSet',
5177 'message' => 'Plugin public_key parameter is not set.',
5178 'code' => 'plugin_public_key_not_set',
5179 'http' => 500,
5180 )
5181 ) );
5182 }
5183
5184 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5185 $this->_plugin :
5186 new FS_Plugin();
5187
5188 $is_premium = $this->get_bool_option( $plugin_info, 'is_premium', true );
5189 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5190
5191 $module_type = $this->get_option( $plugin_info, 'type', $this->_module_type );
5192
5193 $parallel_activation = $this->get_option( $plugin_info, 'parallel_activation' );
5194
5195 if (
5196 ! $is_premium &&
5197 is_array( $parallel_activation ) &&
5198 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type ) &&
5199 $this->get_bool_option( $parallel_activation, 'enabled' )
5200 ) {
5201 $premium_basename = $this->get_option( $parallel_activation, 'premium_version_basename' );
5202
5203 if ( empty( $premium_basename ) ) {
5204 throw new Exception('You need to specify the premium version basename to enable parallel version activation.');
5205 }
5206
5207 $this->_premium_plugin_basename_from_parallel_activation = $premium_basename;
5208
5209 if ( is_plugin_active( $premium_basename ) ) {
5210 $is_premium = true;
5211 }
5212 }
5213
5214 $plugin->update( array(
5215 'id' => $id,
5216 'type' => $module_type,
5217 'public_key' => $public_key,
5218 'slug' => $this->_slug,
5219 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5220 'parent_plugin_id' => $parent_id,
5221 'version' => $this->get_plugin_version(),
5222 'title' => $this->get_plugin_name( $premium_suffix ),
5223 'file' => $this->_plugin_basename,
5224 'is_premium' => $is_premium,
5225 'premium_suffix' => $premium_suffix,
5226 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5227 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5228 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5229 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5230 'opt_in_moderation' => $this->get_option(
5231 $plugin_info,
5232 'opt_in',
5233 // For backward compatibility, we support both parameter names: opt_in and opt_in_moderation.
5234 $this->get_option( $plugin_info, 'opt_in_moderation', null )
5235 ),
5236 ) );
5237
5238 if ( $plugin->is_updated() ) {
5239 // Update plugin details.
5240 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5241 }
5242 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5243 $this->_plugin->secret_key = $secret_key;
5244
5245 /**
5246 * 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).
5247 *
5248 * @author Vova Feldman
5249 * @since 2.4.5
5250 */
5251 if ( $this->is_network_active() && fs_is_network_admin() ) {
5252 if ( isset( $plugin_info['menu_network'] ) &&
5253 is_array( $plugin_info['menu_network'] ) &&
5254 ! empty( $plugin_info['menu_network'] )
5255 ) {
5256 $plugin_info['menu'] = $plugin_info['menu_network'];
5257 }
5258 }
5259
5260 if ( ! isset( $plugin_info['menu'] ) ) {
5261 $plugin_info['menu'] = array();
5262
5263 if ( ! empty( $this->_storage->sdk_last_version ) &&
5264 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5265 ) {
5266 // Backward compatibility to 1.1.2
5267 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5268 $plugin_info['menu_slug'] :
5269 $this->_slug;
5270 }
5271 }
5272
5273 $this->_menu = FS_Admin_Menu_Manager::instance(
5274 $this->_module_id,
5275 $this->_module_type,
5276 $this->get_unique_affix()
5277 );
5278
5279 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5280
5281 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5282 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5283 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5284 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5285 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5286 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5287 if ( $this->_is_premium_only ) {
5288 // If premium only plugin, disable anonymous mode.
5289 $this->_enable_anonymous = false;
5290 $this->_anonymous_mode = false;
5291 } else {
5292 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5293 $this->_anonymous_mode = (
5294 $this->get_bool_option( $plugin_info, 'anonymous_mode', false ) ||
5295 (
5296 $this->apply_filters( 'playground_anonymous_mode', true ) &&
5297 ! empty( $_SERVER['HTTP_HOST'] ) &&
5298 FS_Site::is_playground_wp_environment_by_host( $_SERVER['HTTP_HOST'] )
5299 )
5300 );
5301 }
5302 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5303 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5304
5305 if ( ! empty( $plugin_info['trial'] ) ) {
5306 $this->_trial_days = $this->get_numeric_option(
5307 $plugin_info['trial'],
5308 'days',
5309 // Default to 0 - trial without days specification.
5310 0
5311 );
5312
5313 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5314 }
5315
5316 $this->_navigation = $this->get_option(
5317 $plugin_info,
5318 'navigation',
5319 $this->is_free_wp_org_theme() ?
5320 self::NAVIGATION_TABS :
5321 self::NAVIGATION_MENU
5322 );
5323 }
5324
5325 /**
5326 * @param string[] $options
5327 * @param string $key
5328 * @param mixed $default
5329 *
5330 * @return bool
5331 */
5332 private function get_option( &$options, $key, $default = false ) {
5333 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5334 }
5335
5336 private function get_bool_option( &$options, $key, $default = false ) {
5337 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5338 }
5339
5340 private function get_numeric_option( &$options, $key, $default = false ) {
5341 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5342 }
5343
5344 /**
5345 * Gate keeper.
5346 *
5347 * @author Vova Feldman (@svovaf)
5348 * @since 1.1.7.3
5349 *
5350 * @return bool
5351 */
5352 private function should_stop_execution() {
5353 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5354 /**
5355 * Don't execute Freemius until plugin was fully loaded at least once,
5356 * to give the opportunity for the activation hook to run before pinging
5357 * the API for connectivity test. This logic is relevant for the
5358 * identification of new plugin install vs. plugin update.
5359 *
5360 * @author Vova Feldman (@svovaf)
5361 * @since 1.1.9
5362 */
5363 return true;
5364 }
5365
5366 if ( $this->is_activation_mode() ) {
5367 if ( ! is_admin() ) {
5368 /**
5369 * If in activation mode, don't execute Freemius outside the admin dashboard.
5370 *
5371 * @author Vova Feldman (@svovaf)
5372 * @since 1.1.7.3
5373 */
5374 return true;
5375 }
5376
5377 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5378 /**
5379 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5380 * then don't start Freemius.
5381 *
5382 * @author Vova Feldman (@svovaf)
5383 * @since 1.1.6.3
5384 *
5385 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5386 */
5387 return true;
5388 }
5389
5390 if ( self::is_cron() ) {
5391 /**
5392 * If in activation mode, don't execute Freemius during wp crons
5393 * (wp crons have HTTP context - called as HTTP request).
5394 *
5395 * @author Vova Feldman (@svovaf)
5396 * @since 1.1.7.3
5397 */
5398 return true;
5399 }
5400
5401 if ( self::is_ajax() ) {
5402 /**
5403 * During activation, if running in AJAX mode, unless there's a sticky
5404 * connectivity issue notice, don't run Freemius.
5405 *
5406 * @author Vova Feldman (@svovaf)
5407 * @since 1.1.7.3
5408 */
5409 return true;
5410 }
5411 }
5412
5413 return false;
5414 }
5415
5416 /**
5417 * Triggered after code type has changed.
5418 *
5419 * @author Vova Feldman (@svovaf)
5420 * @since 1.1.9.1
5421 */
5422 function _after_code_type_change() {
5423 $this->_logger->entrance();
5424
5425 if ( $this->is_theme() ) {
5426 // Expire the cache of the previous tabs since the theme may
5427 // have setting updates after code type has changed.
5428 $this->_cache->expire( 'tabs' );
5429 $this->_cache->expire( 'tabs_stylesheets' );
5430 }
5431
5432 if ( ! $this->is_addon() ) {
5433 add_action(
5434 is_admin() ? 'admin_init' : 'init',
5435 array( &$this, '_plugin_code_type_changed' )
5436 );
5437 }
5438
5439 if ( $this->is_registered() && $this->is_premium() ) {
5440 // Purge cached payments after switching to the premium version.
5441 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5442 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5443 }
5444 }
5445
5446 /**
5447 * Handles plugin's code type change (free <--> premium).
5448 *
5449 * @author Vova Feldman (@svovaf)
5450 * @since 1.0.9
5451 */
5452 function _plugin_code_type_changed() {
5453 $this->_logger->entrance();
5454
5455 if ( $this->is_premium() ) {
5456 $this->reconnect_locally();
5457
5458 // Activated premium code.
5459 $this->do_action( 'after_premium_version_activation' );
5460
5461 // Remove all sticky messages related to download of the premium version.
5462 $this->_admin_notices->remove_sticky( array(
5463 'trial_started',
5464 'plan_upgraded',
5465 'plan_changed',
5466 'license_activated',
5467 ) );
5468
5469 $notice = '';
5470 if ( ! $this->is_only_premium() ) {
5471 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5472 }
5473
5474 $license_notice = $this->get_license_network_activation_notice();
5475 if ( ! empty( $license_notice ) ) {
5476 $notice .= ' ' . $license_notice;
5477 }
5478
5479 if ( ! empty( $notice ) ) {
5480 $this->_admin_notices->add_sticky(
5481 trim( $notice ),
5482 'premium_activated',
5483 $this->get_text_x_inline( 'W00t',
5484 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5485 );
5486 }
5487 } else {
5488 // Remove sticky message related to premium code activation.
5489 $this->_admin_notices->remove_sticky( 'premium_activated' );
5490
5491 // Activated free code (after had the premium before).
5492 $this->do_action( 'after_free_version_reactivation' );
5493
5494 if ( $this->is_paying() && ! $this->is_premium() ) {
5495 $this->add_complete_upgrade_instructions_notice(
5496 sprintf(
5497 /* translators: %s: License type (e.g. you have a professional license) */
5498 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5499 $this->get_plan_title()
5500 ),
5501 'plan_upgraded'
5502 );
5503 }
5504 }
5505
5506 if ( $this->is_registered() ) {
5507 // Schedule code type changes event.
5508 $this->maybe_schedule_install_sync_cron();
5509 }
5510
5511 /**
5512 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5513 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5514 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5515 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5516 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5517 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5518 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5519 */
5520 $this->unregister_uninstall_hook();
5521
5522 $this->clear_module_main_file_cache();
5523
5524 // Update is_premium of latest version.
5525 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5526 }
5527
5528 #endregion
5529
5530 #----------------------------------------------------------------------------------
5531 #region Add-ons
5532 #----------------------------------------------------------------------------------
5533
5534 /**
5535 * Check if add-on installed and activated on site.
5536 *
5537 * @author Vova Feldman (@svovaf)
5538 * @since 1.0.6
5539 *
5540 * @param string|number $id_or_slug
5541 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5542 *
5543 * @return bool
5544 */
5545 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5546 $this->_logger->entrance();
5547
5548 $addon_id = self::get_module_id( $id_or_slug );
5549 $is_activated = self::has_instance( $addon_id );
5550
5551 if ( ! $is_activated ) {
5552 return false;
5553 }
5554
5555 if ( is_bool( $is_premium ) ) {
5556 // Check if the specified code version is activate.
5557 $addon = $this->get_addon_instance( $addon_id );
5558 $is_activated = ( $is_premium === $addon->is_premium() );
5559 }
5560
5561 return $is_activated;
5562 }
5563
5564 /**
5565 * Check if add-on was connected to install
5566 *
5567 * @author Vova Feldman (@svovaf)
5568 * @since 1.1.7
5569 *
5570 * @param string|number $id_or_slug
5571 *
5572 * @return bool
5573 */
5574 function is_addon_connected( $id_or_slug ) {
5575 $this->_logger->entrance();
5576
5577 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5578
5579 $addon_id = self::get_module_id( $id_or_slug );
5580 $addon = $this->get_addon( $addon_id );
5581 $slug = $addon->slug;
5582 if ( ! isset( $sites[ $slug ] ) ) {
5583 return false;
5584 }
5585
5586 $site = $sites[ $slug ];
5587
5588 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5589
5590 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5591 // The given slug do NOT belong to any of the plugin's add-ons.
5592 return false;
5593 }
5594
5595 return ( is_object( $site ) &&
5596 is_numeric( $site->id ) &&
5597 is_numeric( $site->user_id ) &&
5598 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5599 );
5600 }
5601
5602 /**
5603 * Determines if add-on installed.
5604 *
5605 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5606 *
5607 * @author Vova Feldman (@svovaf)
5608 * @since 1.0.6
5609 *
5610 * @param string|number $id_or_slug
5611 *
5612 * @return bool
5613 */
5614 function is_addon_installed( $id_or_slug ) {
5615 $this->_logger->entrance();
5616
5617 $addon_id = self::get_module_id( $id_or_slug );
5618
5619 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5620 }
5621
5622 /**
5623 * Get add-on basename.
5624 *
5625 * @author Vova Feldman (@svovaf)
5626 * @since 1.0.6
5627 *
5628 * @param string|number $id_or_slug
5629 *
5630 * @return string
5631 */
5632 function get_addon_basename( $id_or_slug ) {
5633 $addon_id = self::get_module_id( $id_or_slug );
5634
5635 if ( $this->is_addon_activated( $addon_id ) ) {
5636 return self::instance( $addon_id )->get_plugin_basename();
5637 }
5638
5639 $addon = $this->get_addon( $addon_id );
5640 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5641
5642 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5643 return $premium_basename;
5644 }
5645
5646 $all_plugins = $this->get_all_plugins();
5647
5648 foreach ( $all_plugins as $basename => $data ) {
5649 if ( $addon->slug === $data['slug'] ||
5650 $addon->premium_slug === $data['slug']
5651 ) {
5652 return $basename;
5653 }
5654 }
5655
5656 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5657
5658 return $free_basename;
5659 }
5660
5661 /**
5662 * Get installed add-ons instances.
5663 *
5664 * @author Vova Feldman (@svovaf)
5665 * @since 1.0.6
5666 *
5667 * @return Freemius[]
5668 */
5669 function get_installed_addons() {
5670 if ( $this->is_addon() ) {
5671 // Add-on cannot have add-ons.
5672 return array();
5673 }
5674
5675 $installed_addons = array();
5676
5677 foreach ( self::$_instances as $instance ) {
5678 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
5679 $installed_addons[] = $instance;
5680 }
5681 }
5682
5683 return $installed_addons;
5684 }
5685
5686 /**
5687 * Check if any add-ons of the plugin are installed.
5688 *
5689 * @author Leo Fajardo (@leorw)
5690 * @since 1.1.1
5691 *
5692 * @return bool
5693 */
5694 function has_installed_addons() {
5695 if ( ! $this->has_addons() ) {
5696 return false;
5697 }
5698
5699 foreach ( self::$_instances as $instance ) {
5700 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5701 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5702 return true;
5703 }
5704 }
5705 }
5706
5707 return false;
5708 }
5709
5710 /**
5711 * Tell Freemius that the current plugin is an add-on.
5712 *
5713 * @author Vova Feldman (@svovaf)
5714 * @since 1.0.6
5715 *
5716 * @param number $parent_plugin_id The parent plugin ID
5717 */
5718 function init_addon( $parent_plugin_id ) {
5719 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5720 }
5721
5722 /**
5723 * @author Vova Feldman (@svovaf)
5724 * @since 1.0.6
5725 *
5726 * @return bool
5727 */
5728 function is_addon() {
5729 return (
5730 isset( $this->_plugin->parent_plugin_id ) &&
5731 is_numeric( $this->_plugin->parent_plugin_id )
5732 );
5733 }
5734
5735 /**
5736 * @author Vova Feldman (@svovaf)
5737 * @since 2.3.2
5738 *
5739 * @param number $parent_product_id
5740 *
5741 * @return bool
5742 */
5743 function is_addon_of( $parent_product_id ) {
5744 return (
5745 $this->is_addon() &&
5746 $parent_product_id == $this->_plugin->parent_plugin_id
5747 );
5748 }
5749
5750 /**
5751 * Deactivate add-on if it's premium only and the user does't have a valid license.
5752 *
5753 * @param bool $is_after_trial_cancel
5754 *
5755 * @return bool If add-on was deactivated.
5756 */
5757 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5758 if ( ! $this->has_free_plan() &&
5759 ! $this->has_features_enabled_license() &&
5760 ! $this->_has_premium_license()
5761 ) {
5762 if ( $this->is_registered() ) {
5763 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5764 // if (empty($this->_storage->activation_timestamp) ||
5765 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5766 // ) {
5767 /**
5768 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5769 *
5770 * Retry syncing the user add-on licenses.
5771 */
5772 // Sync licenses.
5773 $this->_sync_licenses();
5774 // }
5775
5776 // Try to activate premium license.
5777 $this->_activate_license( true );
5778 }
5779
5780 if ( ! $this->has_free_plan() &&
5781 ! $this->has_features_enabled_license() &&
5782 ! $this->_has_premium_license()
5783 ) {
5784 // @todo Check if deactivate plugins also call the deactivation hook.
5785
5786 $this->_parent->_admin_notices->add_sticky(
5787 sprintf(
5788 ( $is_after_trial_cancel ?
5789 $this->_parent->get_text_inline(
5790 '%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.',
5791 'addon-trial-cancelled-message'
5792 ) :
5793 $this->_parent->get_text_inline(
5794 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5795 'addon-no-license-message'
5796 )
5797 ),
5798 '<b>' . $this->_plugin->title . '</b>'
5799 ) . ' ' . sprintf(
5800 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5801 $this->_parent->addon_url( $this->_slug ),
5802 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5803 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5804 ),
5805 'no_addon_license_' . $this->_slug,
5806 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5807 ( $is_after_trial_cancel ? 'success' : 'error' )
5808 );
5809
5810 deactivate_plugins( array( $this->_plugin_basename ), true );
5811
5812 return true;
5813 }
5814 }
5815
5816 return false;
5817 }
5818
5819 #endregion
5820
5821 #----------------------------------------------------------------------------------
5822 #region Sandbox
5823 #----------------------------------------------------------------------------------
5824
5825 /**
5826 * Set Freemius into sandbox mode for debugging.
5827 *
5828 * @author Vova Feldman (@svovaf)
5829 * @since 1.0.4
5830 *
5831 * @param string $secret_key
5832 */
5833 function init_sandbox( $secret_key ) {
5834 $this->_plugin->secret_key = $secret_key;
5835
5836 // Update plugin details.
5837 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
5838 }
5839
5840 /**
5841 * Check if running payments in sandbox mode.
5842 *
5843 * @author Vova Feldman (@svovaf)
5844 * @since 1.0.4
5845 *
5846 * @return bool
5847 */
5848 function is_payments_sandbox() {
5849 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
5850 }
5851
5852 #endregion
5853
5854 /**
5855 * Check if running test vs. live plugin.
5856 *
5857 * @author Vova Feldman (@svovaf)
5858 * @since 1.0.5
5859 *
5860 * @return bool
5861 */
5862 function is_live() {
5863 return $this->_plugin->is_live;
5864 }
5865
5866 /**
5867 * Check if super-admin skipped connection for all sites in the network.
5868 *
5869 * @author Vova Feldman (@svovaf)
5870 * @since 2.0.0
5871 */
5872 function is_network_anonymous() {
5873 if ( ! $this->_is_network_active ) {
5874 return false;
5875 }
5876
5877 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
5878
5879 if ( empty( $is_anonymous_ms ) ) {
5880 return false;
5881 }
5882
5883 return $is_anonymous_ms['is'];
5884 }
5885
5886 /**
5887 * Check if super-admin opted-in for all sites in the network.
5888 *
5889 * @author Vova Feldman (@svovaf)
5890 * @since 2.0.0
5891 */
5892 function is_network_connected() {
5893 if ( ! $this->_is_network_active ) {
5894 return false;
5895 }
5896
5897 return $this->_storage->get( 'is_network_connected' );
5898 }
5899
5900 /**
5901 * Check if the user skipped connecting the account with Freemius.
5902 *
5903 * @author Vova Feldman (@svovaf)
5904 * @since 1.0.7
5905 *
5906 * @return bool
5907 */
5908 function is_anonymous() {
5909 if ( ! isset( $this->_is_anonymous ) ) {
5910 if ( $this->is_network_anonymous() ) {
5911 $this->_is_anonymous = true;
5912 } else if ( fs_is_network_admin() ) {
5913 /**
5914 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
5915 */
5916 $non_delegated_sites = $this->get_non_delegated_blog_ids();
5917
5918 foreach ( $non_delegated_sites as $blog_id ) {
5919 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5920
5921 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
5922 $this->_is_anonymous = false;
5923 break;
5924 }
5925 }
5926
5927 if ( false !== $this->_is_anonymous ) {
5928 $this->_is_anonymous = true;
5929 }
5930 } else {
5931 if ( ! isset( $this->_storage->is_anonymous ) ) {
5932 // Not skipped.
5933 $this->_is_anonymous = false;
5934 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
5935 // For back compatibility, since the variable was boolean before.
5936 $this->_is_anonymous = $this->_storage->is_anonymous;
5937
5938 // Upgrade stored data format to 1.1.3 format.
5939 $this->set_anonymous_mode( $this->_storage->is_anonymous );
5940 } else {
5941 // Version 1.1.3 and later.
5942 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
5943 }
5944 }
5945 }
5946
5947 return $this->_is_anonymous;
5948 }
5949
5950 /**
5951 * Check if the user skipped the connection of a specified site.
5952 *
5953 * @author Vova Feldman (@svovaf)
5954 * @since 2.0.0
5955 *
5956 * @param int $blog_id
5957 *
5958 * @return bool
5959 */
5960 function is_anonymous_site( $blog_id = 0 ) {
5961 if ( $this->is_network_anonymous() ) {
5962 return true;
5963 }
5964
5965 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5966
5967 if ( empty( $is_anonymous ) ) {
5968 return false;
5969 }
5970
5971 return $is_anonymous['is'];
5972 }
5973
5974 /**
5975 * Check if user connected his account and install pending email activation.
5976 *
5977 * @author Vova Feldman (@svovaf)
5978 * @since 1.0.7
5979 *
5980 * @return bool
5981 */
5982 function is_pending_activation() {
5983 return $this->_storage->get( 'is_pending_activation', false );
5984 }
5985
5986 /**
5987 * @author Leo Fajardo (@leorw)
5988 * @since 2.5.0
5989 */
5990 private function clear_pending_activation_mode() {
5991 // Remove the pending activation sticky notice (if it still exists).
5992 $this->_admin_notices->remove_sticky( 'activation_pending' );
5993
5994 // Clear the plugin's pending activation mode.
5995 unset( $this->_storage->is_pending_activation );
5996 }
5997
5998 /**
5999 * Check if plugin must be WordPress.org compliant.
6000 *
6001 * @since 1.0.7
6002 *
6003 * @return bool
6004 */
6005 function is_org_repo_compliant() {
6006 return $this->_is_org_compliant;
6007 }
6008
6009 #--------------------------------------------------------------------------------
6010 #region WP Cron Common
6011 #--------------------------------------------------------------------------------
6012
6013 /**
6014 * @author Vova Feldman (@svovaf)
6015 * @since 2.0.0
6016 *
6017 * @param string $name Cron name.
6018 *
6019 * @return object
6020 */
6021 private function get_cron_data( $name ) {
6022 $this->_logger->entrance( $name );
6023
6024 /**
6025 * @var object $cron_data
6026 */
6027 return $this->_storage->get( "{$name}_cron", null );
6028 }
6029
6030 /**
6031 * @author Vova Feldman (@svovaf)
6032 * @since 2.0.0
6033 *
6034 * @param string $name Cron name.
6035 */
6036 private function clear_cron_data( $name ) {
6037 $this->_logger->entrance( $name );
6038
6039 $this->_storage->remove( "{$name}_cron" );
6040 }
6041
6042 /**
6043 * @author Vova Feldman (@svovaf)
6044 * @since 2.0.0
6045 *
6046 * @param string $name Cron name.
6047 * @param int $cron_blog_id The cron executing blog ID.
6048 */
6049 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6050 $this->_logger->entrance( $name );
6051
6052 $this->_storage->store( "{$name}_cron", (object) array(
6053 'version' => $this->get_plugin_version(),
6054 'blog_id' => $cron_blog_id,
6055 'sdk_version' => $this->version,
6056 'timestamp' => WP_FS__SCRIPT_START_TIME,
6057 'on' => true,
6058 ) );
6059 }
6060
6061 /**
6062 * Get the cron's executing blog ID.
6063 *
6064 * @author Vova Feldman (@svovaf)
6065 * @since 2.0.0
6066 *
6067 * @param string $name Cron name.
6068 *
6069 * @return int
6070 */
6071 private function get_cron_blog_id( $name ) {
6072 $this->_logger->entrance( $name );
6073
6074 if ( ! is_multisite() ) {
6075 // Not a multisite.
6076 return 0;
6077 }
6078
6079 $cron_data = $this->get_cron_data( $name );
6080
6081 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6082 $cron_data->blog_id :
6083 0;
6084 }
6085
6086 /**
6087 * @author Vova Feldman (@svovaf)
6088 * @since 2.0.0
6089 *
6090 * @param string $name Cron name.
6091 *
6092 * @return bool
6093 */
6094 private function is_cron_on( $name ) {
6095 $this->_logger->entrance( $name );
6096
6097 /**
6098 * @var object $cron_data
6099 */
6100 $cron_data = $this->get_cron_data( $name );
6101
6102 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6103 }
6104
6105 /**
6106 * Unix timestamp for previous cron execution or false if never executed.
6107 *
6108 * @author Vova Feldman (@svovaf)
6109 * @since 2.0.0
6110 *
6111 * @param string $name Cron name.
6112 *
6113 * @return int|false
6114 */
6115 private function cron_last_execution( $name ) {
6116 $this->_logger->entrance( $name );
6117
6118 return $this->_storage->get( "{$name}_timestamp" );
6119 }
6120
6121 /**
6122 * Set cron execution time to now.
6123 *
6124 * @author Vova Feldman (@svovaf)
6125 * @since 2.0.0
6126 *
6127 * @param string $name Cron name.
6128 */
6129 private function set_cron_execution_timestamp( $name ) {
6130 $this->_logger->entrance( $name );
6131
6132 $this->_storage->store( "{$name}_timestamp", time() );
6133 }
6134
6135 /**
6136 * Sets the keepalive time to now.
6137 *
6138 * @author Leo Fajardo (@leorw)
6139 * @since 2.2.3
6140 *
6141 * @param bool|null $use_network_level_storage
6142 */
6143 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6144 $this->_logger->entrance();
6145
6146 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6147 }
6148
6149 /**
6150 * Check if cron was executed in the last $period of seconds.
6151 *
6152 * @author Vova Feldman (@svovaf)
6153 * @since 2.0.0
6154 *
6155 * @param string $name Cron name.
6156 * @param int $period In seconds
6157 *
6158 * @return bool
6159 */
6160 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6161 $this->_logger->entrance( $name );
6162
6163 $last_execution = $this->cron_last_execution( $name );
6164
6165 if ( ! is_numeric( $last_execution ) ) {
6166 return false;
6167 }
6168
6169 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6170 }
6171
6172 /**
6173 * WP Cron is executed on a site level. When running in a multisite network environment
6174 * with the network integration activated, for optimization reasons, we are consolidating
6175 * the installs data sync cron to be executed only from a single site.
6176 *
6177 * @author Vova Feldman (@svovaf)
6178 * @since 2.0.0
6179 *
6180 * @param int $except_blog_id Target any except the excluded blog ID.
6181 *
6182 * @return int
6183 */
6184 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6185 if ( ! is_multisite() ) {
6186 return 0;
6187 }
6188
6189 if ( $this->_is_network_active ) {
6190 $network_install_blog_id = $this->_storage->network_install_blog_id;
6191
6192 if (
6193 is_numeric( $network_install_blog_id ) &&
6194 $except_blog_id != $network_install_blog_id &&
6195 self::is_site_active( $network_install_blog_id )
6196 ) {
6197 // Try to run cron from the main network blog.
6198 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6199
6200 if (
6201 is_object( $install ) &&
6202 $this->is_tracking_allowed( $network_install_blog_id, $install )
6203 ) {
6204 return $network_install_blog_id;
6205 }
6206 }
6207 }
6208
6209 // Get first opted-in blog ID with active tracking.
6210 $installs = $this->get_blog_install_map();
6211 foreach ( $installs as $blog_id => $install ) {
6212 if ( $except_blog_id != $blog_id &&
6213 self::is_site_active( $blog_id ) &&
6214 $this->is_tracking_allowed( $blog_id, $install )
6215 ) {
6216 return $blog_id;
6217 }
6218 }
6219
6220 return 0;
6221 }
6222
6223 /**
6224 * @author Vova Feldman (@svovaf)
6225 * @since 2.0.0
6226 *
6227 * @param string $name Cron name.
6228 * @param string $action_tag Callback action tag.
6229 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6230 */
6231 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6232 $this->_logger->entrance( $name );
6233
6234 if ( ! $this->is_cron_on( $name ) ) {
6235 return;
6236 }
6237
6238 $clear_cron = true;
6239 if ( ! $is_network_clear && $this->_is_network_active ) {
6240 $installs = $this->get_blog_install_map();
6241
6242 foreach ( $installs as $blog_id => $install ) {
6243 /**
6244 * @var FS_Site $install
6245 */
6246 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6247 $clear_cron = false;
6248 break;
6249 }
6250 }
6251 }
6252
6253 if ( ! $clear_cron ) {
6254 return;
6255 }
6256
6257 $cron_blog_id = $this->get_cron_blog_id( $name );
6258
6259 $this->clear_cron_data( $name );
6260
6261 if ( 0 < $cron_blog_id ) {
6262 switch_to_blog( $cron_blog_id );
6263 }
6264
6265 if ( empty( $action_tag ) ) {
6266 $action_tag = $name;
6267 }
6268
6269 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6270
6271 if ( 0 < $cron_blog_id ) {
6272 restore_current_blog();
6273 }
6274 }
6275
6276 /**
6277 * Unix timestamp for next cron execution or false if not scheduled.
6278 *
6279 * @author Vova Feldman (@svovaf)
6280 * @since 2.0.0
6281 *
6282 * @param string $name Cron name.
6283 * @param string $action_tag Callback action tag.
6284 *
6285 * @return int|false
6286 */
6287 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6288 $this->_logger->entrance( $name );
6289
6290 if ( ! $this->is_cron_on( $name ) ) {
6291 return false;
6292 }
6293
6294 $cron_blog_id = $this->get_cron_blog_id( $name );
6295
6296 if ( 0 < $cron_blog_id ) {
6297 switch_to_blog( $cron_blog_id );
6298 }
6299
6300 if ( empty( $action_tag ) ) {
6301 $action_tag = $name;
6302 }
6303
6304 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6305
6306 if ( 0 < $cron_blog_id ) {
6307 restore_current_blog();
6308 }
6309
6310 return $next_scheduled;
6311 }
6312
6313 /**
6314 * @author Vova Feldman (@svovaf)
6315 * @since 2.0.0
6316 *
6317 * @param string $name Cron name.
6318 * @param string $action_tag Callback action tag.
6319 * @param string $recurrence 'single' or 'daily'.
6320 * @param int $start_at Defaults to now.
6321 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6322 * @param int $except_blog_id Target any except the excluded blog ID.
6323 */
6324 private function schedule_cron(
6325 $name,
6326 $action_tag = '',
6327 $recurrence = 'single',
6328 $start_at = WP_FS__SCRIPT_START_TIME,
6329 $randomize_start = true,
6330 $except_blog_id = 0
6331 ) {
6332 $this->_logger->entrance( $name );
6333
6334 $this->clear_cron( $name, $action_tag, true );
6335
6336 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6337
6338 if ( is_multisite() && 0 == $cron_blog_id ) {
6339 // Don't schedule cron since couldn't find a target blog.
6340 return;
6341 }
6342
6343 if ( 0 < $cron_blog_id ) {
6344 switch_to_blog( $cron_blog_id );
6345 }
6346
6347 if ( 'daily' === $recurrence ) {
6348 if ( $randomize_start ) {
6349 // Schedule first sync with a random 12 hour time range from now.
6350 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6351 }
6352
6353 // Schedule daily WP cron.
6354 wp_schedule_event(
6355 $start_at,
6356 'daily',
6357 $this->get_action_tag( $action_tag )
6358 );
6359 } else if ( 'single' === $recurrence ) {
6360 // Schedule single cron.
6361 wp_schedule_single_event(
6362 $start_at,
6363 $this->get_action_tag( $action_tag )
6364 );
6365 }
6366
6367 $this->set_cron_data( $name, $cron_blog_id );
6368
6369 if ( 0 < $cron_blog_id ) {
6370 restore_current_blog();
6371 }
6372 }
6373
6374 /**
6375 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6376 * that doesn't halt page loading.
6377 *
6378 * @author Vova Feldman (@svovaf)
6379 * @since 2.0.0
6380 *
6381 * @param string $name Cron name.
6382 * @param callable $callable The function that should be executed.
6383 */
6384 private function execute_cron( $name, $callable ) {
6385 $this->_logger->entrance( $name );
6386
6387 // Store the last time data sync was executed.
6388 $this->set_cron_execution_timestamp( $name );
6389
6390 // Check if API is temporary down.
6391 if ( FS_Api::is_temporary_down() ) {
6392 return;
6393 }
6394
6395 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6396
6397 $users_2_blog_ids = array();
6398
6399 if ( ! is_multisite() ) {
6400 // Add dummy blog.
6401 $users_2_blog_ids[0] = array( 0 );
6402 } else {
6403 $installs = $this->get_blog_install_map();
6404 foreach ( $installs as $blog_id => $install ) {
6405 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6406 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6407 $users_2_blog_ids[ $install->user_id ] = array();
6408 }
6409
6410 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6411 }
6412 }
6413 }
6414
6415 $current_blog_id = get_current_blog_id();
6416
6417 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6418 if ( 0 < $blog_ids[0] ) {
6419 $this->switch_to_blog( $blog_ids[0] );
6420 }
6421
6422 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6423
6424 foreach ( $blog_ids as $blog_id ) {
6425 $this->do_action( "after_{$name}_cron", $blog_id );
6426 }
6427 }
6428
6429 if ( is_multisite() ) {
6430 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6431
6432 $this->do_action( "after_{$name}_cron_multisite" );
6433 }
6434 }
6435
6436 #endregion
6437
6438 #----------------------------------------------------------------------------------
6439 #region Daily Sync Cron
6440 #----------------------------------------------------------------------------------
6441
6442
6443 /**
6444 * @author Vova Feldman (@svovaf)
6445 * @since 2.0.0
6446 *
6447 * @return bool
6448 */
6449 private function is_sync_cron_scheduled() {
6450 return $this->is_cron_on( 'sync' );
6451 }
6452
6453 /**
6454 * Get the sync cron's executing blog ID.
6455 *
6456 * @author Vova Feldman (@svovaf)
6457 * @since 2.0.0
6458 *
6459 * @return int
6460 */
6461 private function get_sync_cron_blog_id() {
6462 return $this->get_cron_blog_id( 'sync' );
6463 }
6464
6465 /**
6466 * @author Vova Feldman (@svovaf)
6467 * @since 1.1.7.3
6468 */
6469 private function run_manual_sync() {
6470 if ( ! $this->is_user_admin() ) {
6471 return;
6472 }
6473
6474 // Run manual sync.
6475 $this->_sync_cron();
6476
6477 // Reschedule next cron to run 24 hours from now (performance optimization).
6478 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6479 }
6480
6481 /**
6482 * Data sync cron job. Replaces the background sync non blocking HTTP request
6483 * that doesn't halt page loading.
6484 *
6485 * @author Vova Feldman (@svovaf)
6486 * @since 1.1.7.3
6487 * @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.
6488 */
6489 function _sync_cron() {
6490 $this->_logger->entrance();
6491
6492 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6493 }
6494
6495 /**
6496 * The actual data sync cron logic.
6497 *
6498 * @author Vova Feldman (@svovaf)
6499 * @since 2.0.0
6500 *
6501 * @param int[] $blog_ids
6502 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6503 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6504 * updates for a single site in case `execute_cron` has switched to a different blog.
6505 */
6506 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6507 if ( $this->is_registered() ) {
6508 if ( $this->has_paid_plan() ) {
6509 // Initiate background plan sync.
6510 $this->_sync_license( true, false, $current_blog_id );
6511
6512 if ( $this->is_paying() ) {
6513 // Check for premium plugin updates.
6514 $this->check_updates( true );
6515 }
6516 } else {
6517 // Sync install(s) (only if something changed locally).
6518 if ( 1 < count( $blog_ids ) ) {
6519 $this->sync_installs();
6520 } else {
6521 $this->sync_install();
6522 }
6523
6524 $this->maybe_sync_install_user();
6525 }
6526 }
6527 }
6528
6529 /**
6530 * Check if sync was executed in the last $period of seconds.
6531 *
6532 * @author Vova Feldman (@svovaf)
6533 * @since 1.1.7.3
6534 *
6535 * @param int $period In seconds
6536 *
6537 * @return bool
6538 */
6539 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6540 return $this->is_cron_executed( 'sync', $period );
6541 }
6542
6543 /**
6544 * @author Vova Feldman (@svovaf)
6545 * @since 1.1.7.3
6546 *
6547 * @return bool
6548 */
6549 private function is_sync_cron_on() {
6550 return $this->is_cron_on( 'sync' );
6551 }
6552
6553 /**
6554 * @author Leo Fajardo (@leorw)
6555 * @since 2.5.0
6556 */
6557 private function maybe_schedule_sync_cron() {
6558 $next_schedule = $this->next_sync_cron();
6559
6560 // The event is properly scheduled, so no need to reschedule it.
6561 if (
6562 is_numeric( $next_schedule ) &&
6563 $next_schedule > time()
6564 ) {
6565 return;
6566 }
6567
6568 $this->schedule_sync_cron();
6569 }
6570
6571 /**
6572 * Instead of running blocking install sync event, execute non blocking scheduled cron job.
6573 *
6574 * @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 specified blog ID from being the cron job executor.
6575 *
6576 * @author Leo Fajardo (@leorw)
6577 * @since 2.9.1
6578 */
6579 private function maybe_schedule_install_sync_cron( $except_blog_id = 0 ) {
6580 if ( ! $this->is_user_in_admin() ) {
6581 return;
6582 }
6583
6584 if ( $this->is_clone() ) {
6585 return;
6586 }
6587
6588 if (
6589 // The event has been properly scheduled, so no need to reschedule it.
6590 is_numeric( $this->next_install_sync() )
6591 ) {
6592 return;
6593 }
6594
6595 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6596 }
6597
6598 /**
6599 * @author Vova Feldman (@svovaf)
6600 * @since 1.1.7.3
6601 *
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 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.
6605 */
6606 private function schedule_sync_cron(
6607 $start_at = WP_FS__SCRIPT_START_TIME,
6608 $randomize_start = true,
6609 $except_blog_id = 0
6610 ) {
6611 $this->schedule_cron(
6612 'sync',
6613 'data_sync',
6614 'daily',
6615 $start_at,
6616 $randomize_start,
6617 $except_blog_id
6618 );
6619 }
6620
6621 /**
6622 * Add the actual sync function to the cron job hook.
6623 *
6624 * @author Vova Feldman (@svovaf)
6625 * @since 1.1.7.3
6626 */
6627 private function hook_callback_to_sync_cron() {
6628 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6629 }
6630
6631 /**
6632 * @author Vova Feldman (@svovaf)
6633 * @since 1.1.7.3
6634 *
6635 * @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.
6636 */
6637 private function clear_sync_cron( $is_network_clear = false ) {
6638 $this->_logger->entrance();
6639
6640 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6641 }
6642
6643 /**
6644 * Unix timestamp for next sync cron execution or false if not scheduled.
6645 *
6646 * @author Vova Feldman (@svovaf)
6647 * @since 1.1.7.3
6648 *
6649 * @return int|false
6650 */
6651 function next_sync_cron() {
6652 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6653 }
6654
6655 /**
6656 * Unix timestamp for previous sync cron execution or false if never executed.
6657 *
6658 * @author Vova Feldman (@svovaf)
6659 * @since 1.1.7.3
6660 *
6661 * @return int|false
6662 */
6663 function last_sync_cron() {
6664 return $this->cron_last_execution( 'sync' );
6665 }
6666
6667 #endregion Daily Sync Cron ------------------------------------------------------------------
6668
6669 #----------------------------------------------------------------------------------
6670 #region Async Install Sync
6671 #----------------------------------------------------------------------------------
6672
6673 /**
6674 * @author Vova Feldman (@svovaf)
6675 * @since 1.1.7.3
6676 *
6677 * @return bool
6678 */
6679 private function is_install_sync_scheduled() {
6680 return $this->is_cron_on( 'install_sync' );
6681 }
6682
6683 /**
6684 * Get the sync cron's executing blog ID.
6685 *
6686 * @author Vova Feldman (@svovaf)
6687 * @since 2.0.0
6688 *
6689 * @return int
6690 */
6691 private function get_install_sync_cron_blog_id() {
6692 return $this->get_cron_blog_id( 'install_sync' );
6693 }
6694
6695 /**
6696 * Unix timestamp for previous install sync cron execution or false if never executed.
6697 *
6698 * @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.
6699 *
6700 * @author Vova Feldman (@svovaf)
6701 * @since 1.1.7.3
6702 *
6703 * @return int|false
6704 */
6705 function last_install_sync() {
6706 return $this->cron_last_execution( 'install_sync' );
6707 }
6708
6709 /**
6710 * Unix timestamp for next install sync cron execution or false if not scheduled.
6711 *
6712 * @author Vova Feldman (@svovaf)
6713 * @since 1.1.7.3
6714 *
6715 * @return int|false
6716 */
6717 function next_install_sync() {
6718 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6719 }
6720
6721 /**
6722 * Add the actual install sync function to the cron job hook.
6723 *
6724 * @author Vova Feldman (@svovaf)
6725 * @since 1.1.7.3
6726 */
6727 private function hook_callback_to_install_sync() {
6728 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6729 }
6730
6731 /**
6732 * @author Vova Feldman (@svovaf)
6733 * @since 1.1.7.3
6734 *
6735 * @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.
6736 */
6737 private function clear_install_sync_cron( $is_network_clear = false ) {
6738 $this->_logger->entrance();
6739
6740 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6741 }
6742
6743 /**
6744 * @author Vova Feldman (@svovaf)
6745 * @since 1.1.7.3
6746 * @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.
6747 */
6748 public function _run_sync_install() {
6749 $this->_logger->entrance();
6750
6751 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6752 }
6753
6754 /**
6755 * The actual install(s) sync cron logic.
6756 *
6757 * @author Vova Feldman (@svovaf)
6758 * @since 2.0.0
6759 *
6760 * @param int[] $blog_ids
6761 * @param int|null $current_blog_id
6762 */
6763 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6764 if ( $this->is_registered() ) {
6765 if ( 1 < count( $blog_ids ) ) {
6766 $this->sync_installs( array(), true );
6767 } else {
6768 $this->sync_install( array(), true );
6769 }
6770
6771 $this->maybe_sync_install_user();
6772 }
6773 }
6774
6775 #endregion Async Install Sync ------------------------------------------------------------------
6776
6777 /**
6778 * Show a notice that activation is currently pending.
6779 *
6780 * @todo Add some sort of mechanism to allow users to update the email address they would like to opt-in with when $is_suspicious_email is true.
6781 *
6782 * @author Vova Feldman (@svovaf)
6783 * @since 1.0.7
6784 *
6785 * @param bool|string $email_address
6786 * @param bool $is_pending_trial Since 1.2.1.5
6787 * @param bool $is_suspicious_email Since 2.5.0 Set to true when there's an indication that email address the user opted in with is fake/dummy/placeholder.
6788 * @param bool $has_upgrade_context Since 2.5.3
6789 * @param bool $support_email_address Since 2.5.3
6790 */
6791 function _add_pending_activation_notice(
6792 $email_address = false,
6793 $is_pending_trial = false,
6794 $is_suspicious_email = false,
6795 $has_upgrade_context = false,
6796 $support_email_address = false
6797 ) {
6798 if ( ! is_string( $email_address ) ) {
6799 $current_user = self::_get_current_wp_user();
6800 $email_address = $current_user->user_email;
6801 }
6802
6803 $formatted_message_args = array(
6804 "<b>{$this->get_plugin_name()}</b>",
6805 "<b>{$email_address}</b>",
6806 );
6807
6808 if ( ! $has_upgrade_context || ! fs_is_network_admin() ) {
6809 /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */
6810 $formatted_message = $this->get_text_inline( 'You should receive a confirmation email for %1$s to your mailbox at %2$s. Please make sure you click the button in that email to %3$s.', 'pending-activation-message' );
6811
6812 $formatted_message_args[] = $is_pending_trial ?
6813 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6814 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' );
6815
6816 $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' );
6817 } else {
6818 /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */
6819 $formatted_message = $this->get_text_inline( 'You should receive %3$s for %1$s to your mailbox at %2$s in the next 5 minutes.' );
6820
6821 if ( $this->has_release_on_freemius() ) {
6822 $formatted_message_args[] = $this->get_text_x_inline(
6823 'the installation instructions',
6824 'Part of the message telling the user what they should receive via email.',
6825 'the-installation-instructions-phrase'
6826 );
6827 } else {
6828 $formatted_message_args[] = $this->get_text_x_inline(
6829 'a license key',
6830 'Part of the message telling the user what they should receive via email.',
6831 'a-license-key-phrase'
6832 );
6833
6834 $formatted_message .= ( ' ' . sprintf(
6835 /* translators: %s: activation link (e.g.: <a>Click here</a>) */
6836 $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ),
6837 sprintf(
6838 '<b><a href="%s">%s</a></b>',
6839 $this->get_activation_url( array(
6840 'fs_action' => 'reset_pending_activation_mode',
6841 'require_license' => 'true',
6842 'fs_unique_affix' => $this->get_unique_affix(),
6843 ) ),
6844 $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' )
6845 )
6846 ) );
6847 }
6848
6849 $formatted_message_args[] = ( ! empty( $support_email_address ) ) ?
6850 ( "<b>{$support_email_address}</b>" ) :
6851 $this->get_text_x_inline(
6852 "the product's support email address",
6853 'Part of the message that tells the user to check their spam folder for a specific email.',
6854 'product-support-email-address-phrase'
6855 );
6856
6857 $formatted_message .= ( ' ' . $this->get_text_inline( 'If you didn\'t get the email, try checking your spam folder or search for emails from %4$s.', 'check-spam-folder-message' ) );
6858
6859 $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' );
6860 }
6861
6862 $this->_admin_notices->add_sticky(
6863 vsprintf( $formatted_message, $formatted_message_args ),
6864 'activation_pending',
6865 $notice_title
6866 );
6867 }
6868
6869 /**
6870 * Check if currently in plugin activation.
6871 *
6872 * @author Vova Feldman (@svovaf)
6873 * @since 1.1.4
6874 *
6875 * @return bool
6876 */
6877 function is_plugin_activation() {
6878 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6879
6880 return !empty($result);
6881 }
6882
6883 /**
6884 *
6885 * NOTE: admin_menu action executed before admin_init.
6886 *
6887 * @author Vova Feldman (@svovaf)
6888 * @since 1.0.7
6889 */
6890 function _admin_init_action() {
6891 $is_migration = $this->is_migration();
6892
6893 /**
6894 * Automatically redirect to connect/activation page after plugin activation.
6895 *
6896 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6897 */
6898 if ( $this->is_plugin_activation() ) {
6899 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6900
6901 if ( isset( $_GET['activate-multi'] ) ) {
6902 /**
6903 * Don't redirect if activating multiple plugins at once (bulk activation).
6904 */
6905 } else if (
6906 self::is_deactivation_snoozed() &&
6907 (
6908 // Either running the free code base.
6909 ! $this->is_premium() ||
6910 // Or if has a free version.
6911 ! $this->is_only_premium() ||
6912 // If premium only, don't redirect if license is activated.
6913 ( $this->is_registered() && ! $this->can_use_premium_code() )
6914 )
6915 ) {
6916 /**
6917 * Don't redirect if activating during the deactivation snooze period (aka troubleshooting), unless activating a paid product version that the admin didn't enter its license key yet.
6918 */
6919 } else if ( ! $is_migration ) {
6920 $this->_redirect_on_activation_hook();
6921 return;
6922 }
6923 }
6924
6925 if ( $is_migration ) {
6926 return;
6927 }
6928
6929 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6930 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6931
6932 $this->skip_connection( fs_is_network_admin() );
6933
6934 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6935 }
6936
6937 if ( $this->is_network_activation_mode() &&
6938 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6939 ) {
6940 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6941
6942 $this->delegate_connection();
6943
6944 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6945 }
6946
6947 $this->_add_upgrade_action_link();
6948
6949 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
6950 (
6951 ( true === $this->_storage->require_license_activation ) ||
6952 // Not registered nor anonymous.
6953 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6954 // OR, network level and in network upgrade mode.
6955 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6956 )
6957 ) {
6958 if ( ! $this->is_pending_activation() ) {
6959 if ( ! $this->is_activation_page() ) {
6960 /**
6961 * If a user visits any other admin page before activating the premium-only theme with a valid
6962 * license, reactivate the previous theme.
6963 *
6964 * @author Leo Fajardo (@leorw)
6965 * @since 1.2.2
6966 */
6967 if ( $this->is_theme() &&
6968 ! $this->has_settings_menu() &&
6969 ! isset( $_REQUEST['fs_action'] ) &&
6970 $this->can_activate_previous_theme()
6971 ) {
6972 if ( $this->is_only_premium() ) {
6973 $this->activate_previous_theme();
6974 return;
6975 }
6976
6977 if ( true === $this->_storage->require_license_activation ) {
6978 $this->_storage->require_license_activation = false;
6979 }
6980 }
6981
6982 if ( ! fs_is_network_admin() &&
6983 $this->is_network_activation_mode() &&
6984 ! $this->is_delegated_connection()
6985 ) {
6986 return;
6987 }
6988
6989 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6990 if ( ! $this->_anonymous_mode &&
6991 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
6992 // Show notice for new plugin installations.
6993 $this->_admin_notices->add(
6994 sprintf(
6995 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6996 sprintf( '<b><a href="%s">%s</a></b>',
6997 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6998 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
6999 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7000 )
7001 ),
7002 '',
7003 'update-nag'
7004 );
7005 }
7006 } else {
7007 if ( $this->should_add_sticky_optin_notice() ) {
7008 $this->add_sticky_optin_admin_notice();
7009 }
7010
7011 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7012 // Don't show admin nag if plugin update.
7013 wp_enqueue_script( 'wp-pointer' );
7014 wp_enqueue_style( 'wp-pointer' );
7015
7016 $this->_enqueue_connect_essentials();
7017
7018 add_action( 'admin_print_footer_scripts', array(
7019 $this,
7020 '_add_connect_pointer_script'
7021 ) );
7022 }
7023 }
7024 }
7025 }
7026
7027 if ( $this->show_opt_in_on_themes_page() &&
7028 $this->is_activation_page()
7029 ) {
7030 $this->_show_theme_activation_optin_dialog();
7031 }
7032 }
7033 }
7034
7035 /**
7036 * @author Vova Feldman (@svovaf)
7037 * @since 2.0.0
7038 *
7039 * @return bool
7040 */
7041 private function should_add_sticky_optin_notice() {
7042 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7043 return false;
7044 }
7045
7046 if ( fs_is_network_admin() ) {
7047 if ( ! $this->_is_network_active ) {
7048 return false;
7049 }
7050
7051 if ( ! $this->is_network_activation_mode() ) {
7052 return false;
7053 }
7054
7055 return ! isset( $this->_storage->sticky_optin_added_ms );
7056 }
7057
7058 if ( ! $this->is_activation_mode() ) {
7059 return false;
7060 }
7061
7062 // If running from a blog admin and delegated the connection.
7063 return ! isset( $this->_storage->sticky_optin_added );
7064 }
7065
7066 /**
7067 * @author Leo Fajardo (@leorw)
7068 * @since 2.0.0
7069 */
7070 private function add_sticky_optin_admin_notice() {
7071 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7072 $this->_storage->sticky_optin_added = true;
7073 } else {
7074 $this->_storage->sticky_optin_added_ms = true;
7075 }
7076
7077 // Show notice for new plugin installations.
7078 $this->_admin_notices->add_sticky(
7079 sprintf(
7080 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7081 $this->_module_type,
7082 sprintf( '<b><a href="%s">%s</a></b>',
7083 $this->get_activation_url(),
7084 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7085 )
7086 ),
7087 'connect_account',
7088 '',
7089 'update-nag'
7090 );
7091 }
7092
7093 /**
7094 * Enqueue connect requires scripts and styles.
7095 *
7096 * @author Vova Feldman (@svovaf)
7097 * @since 1.1.4
7098 */
7099 function _enqueue_connect_essentials() {
7100 wp_enqueue_script( 'jquery' );
7101 wp_enqueue_script( 'json2' );
7102
7103 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
7104 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7105 }
7106
7107 /**
7108 * Add connect / opt-in pointer.
7109 *
7110 * @author Vova Feldman (@svovaf)
7111 * @since 1.1.4
7112 */
7113 function _add_connect_pointer_script() {
7114 $vars = array( 'id' => $this->_module_id );
7115 $pointer_content = fs_get_template( 'connect.php', $vars );
7116 ?>
7117 <script type="text/javascript">// <![CDATA[
7118 jQuery(document).ready(function ($) {
7119 if ('undefined' !== typeof(jQuery().pointer)) {
7120
7121 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7122
7123 if (element.length > 0) {
7124 var optin = $(element).pointer($.extend(true, {}, {
7125 content : <?php echo json_encode( $pointer_content ) ?>,
7126 position : {
7127 edge : 'left',
7128 align: 'center'
7129 },
7130 buttons : function () {
7131 // Don't show pointer buttons.
7132 return '';
7133 },
7134 pointerWidth: 482
7135 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7136
7137 <?php
7138 echo $this->apply_filters( 'optin_pointer_execute', "
7139
7140 optin.pointer('open');
7141
7142 // Tag the opt-in pointer with custom class.
7143 $('.wp-pointer #fs_connect')
7144 .parents('.wp-pointer.wp-pointer-top')
7145 .addClass('fs-opt-in-pointer');
7146
7147 ", 'element', 'optin' ) ?>
7148 }
7149 }
7150 });
7151 // ]]></script>
7152 <?php
7153 }
7154
7155 /**
7156 * Return current page's URL.
7157 *
7158 * @author Vova Feldman (@svovaf)
7159 * @since 1.0.7
7160 *
7161 * @return string
7162 */
7163 static function current_page_url() {
7164 $url = 'http';
7165
7166 if ( isset( $_SERVER["HTTPS"] ) ) {
7167 if ( $_SERVER["HTTPS"] == "on" ) {
7168 $url .= "s";
7169 }
7170 }
7171 $url .= "://";
7172 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7173 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7174 } else {
7175 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7176 }
7177
7178 return esc_url( $url );
7179 }
7180
7181 /**
7182 * Check if the current page is the plugin's main admin settings page.
7183 *
7184 * @author Vova Feldman (@svovaf)
7185 * @since 1.0.7
7186 *
7187 * @return bool
7188 */
7189 function _is_plugin_page() {
7190 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7191 fs_is_plugin_page( $this->_slug );
7192 }
7193
7194 /* Events
7195 ------------------------------------------------------------------------------------------------------------------*/
7196 /**
7197 * Delete site install from Database.
7198 *
7199 * @author Vova Feldman (@svovaf)
7200 * @since 1.0.1
7201 *
7202 * @param bool $store
7203 * @param int|null $blog_id Since 2.0.0
7204 *
7205 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7206 */
7207 function _delete_site( $store = true, $blog_id = null ) {
7208 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7209 }
7210
7211 /**
7212 * Delete site install from Database.
7213 *
7214 * @author Vova Feldman (@svovaf)
7215 * @since 1.2.2.7
7216 *
7217 * @param string $slug
7218 * @param string $module_type
7219 * @param bool $store
7220 * @param int|null $blog_id Since 2.0.0
7221 *
7222 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7223 */
7224 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7225 $sites = self::get_all_sites( $module_type, $blog_id );
7226
7227 $install_id = false;
7228
7229 if ( isset( $sites[ $slug ] ) ) {
7230 if ( is_object( $sites[ $slug ] ) ) {
7231 $install_id = $sites[ $slug ]->id;
7232 }
7233
7234 unset( $sites[ $slug ] );
7235
7236 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7237 }
7238
7239 return $install_id;
7240 }
7241
7242 /**
7243 * Delete plugin's plans information.
7244 *
7245 * @param bool $store Flush to Database if true.
7246 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7247 *
7248 * @author Vova Feldman (@svovaf)
7249 * @since 1.0.9
7250 */
7251 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7252 $this->_logger->entrance();
7253
7254 $plans = self::get_all_plans( $this->_module_type );
7255
7256 $plans_to_keep = array();
7257
7258 if ( $keep_associated_plans ) {
7259 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7260 foreach ( $plans_ids_to_keep as $plan_id ) {
7261 $plan = self::_get_plan_by_id( $plan_id );
7262 if ( is_object( $plan ) ) {
7263 $plans_to_keep[] = self::_encrypt_entity( $plan );
7264 }
7265 }
7266 }
7267
7268 if ( ! empty( $plans_to_keep ) ) {
7269 $plans[ $this->_slug ] = $plans_to_keep;
7270 } else {
7271 unset( $plans[ $this->_slug ] );
7272 }
7273
7274 $this->set_account_option( 'plans', $plans, $store );
7275 }
7276
7277 /**
7278 * Delete all plugin licenses.
7279 *
7280 * @author Vova Feldman (@svovaf)
7281 * @since 1.0.9
7282 *
7283 * @param bool $store
7284 */
7285 private function _delete_licenses( $store = true ) {
7286 $this->_logger->entrance();
7287
7288 $all_licenses = self::get_all_licenses();
7289
7290 unset( $all_licenses[ $this->_module_id ] );
7291
7292 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7293 }
7294
7295 /**
7296 * Check if Freemius was added on new plugin installation.
7297 *
7298 * @author Vova Feldman (@svovaf)
7299 * @since 1.1.5
7300 *
7301 * @return bool
7302 */
7303 function is_plugin_new_install() {
7304 return isset( $this->_storage->is_plugin_new_install ) &&
7305 $this->_storage->is_plugin_new_install;
7306 }
7307
7308 /**
7309 * Check if it's the first plugin release that is running Freemius.
7310 *
7311 * @author Vova Feldman (@svovaf)
7312 * @since 1.2.1.5
7313 *
7314 * @return bool
7315 */
7316 function is_first_freemius_powered_version() {
7317 return empty( $this->_storage->plugin_last_version );
7318 }
7319
7320 /**
7321 * @author Leo Fajardo (@leorw)
7322 * @since 1.2.2
7323 *
7324 * @return bool|string
7325 */
7326 private function get_previous_theme_slug() {
7327 return isset( $this->_storage->previous_theme ) ?
7328 $this->_storage->previous_theme :
7329 false;
7330 }
7331
7332 /**
7333 * @author Leo Fajardo (@leorw)
7334 * @since 1.2.2
7335 *
7336 * @return bool
7337 */
7338 private function can_activate_previous_theme() {
7339 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7340 }
7341
7342 /**
7343 * @author Leo Fajardo (@leorw)
7344 * @since 2.5.0
7345 *
7346 * @return bool
7347 */
7348 private function can_activate_theme( $slug ) {
7349 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7350 $theme_instance = wp_get_theme( $slug );
7351
7352 return $theme_instance->exists();
7353 }
7354
7355 return false;
7356 }
7357
7358 /**
7359 * @author Leo Fajardo (@leorw)
7360 * @since 1.2.2
7361 */
7362 private function activate_previous_theme() {
7363 switch_theme( $this->get_previous_theme_slug() );
7364 unset( $this->_storage->previous_theme );
7365
7366 global $pagenow;
7367 if ( 'themes.php' === $pagenow ) {
7368 /**
7369 * Refresh the active theme information.
7370 *
7371 * @author Leo Fajardo (@leorw)
7372 * @since 1.2.2
7373 */
7374 fs_redirect( $this->admin_url( $pagenow ) );
7375 }
7376 }
7377
7378 /**
7379 * @author Leo Fajardo (@leorw)
7380 * @since 1.2.2
7381 *
7382 * @return string
7383 */
7384 function get_previous_theme_activation_url() {
7385 if ( ! $this->can_activate_previous_theme() ) {
7386 return '';
7387 }
7388
7389 /**
7390 * Activation URL
7391 *
7392 * @author Leo Fajardo (@leorw)
7393 * @since 1.2.2
7394 */
7395 return wp_nonce_url(
7396 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7397 'switch-theme_' . $this->get_previous_theme_slug()
7398 );
7399 }
7400
7401 /**
7402 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7403 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7404 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7405 * theme doesn't exist, then there will be no close button.
7406 *
7407 * @author Leo Fajardo (@leorw)
7408 * @since 1.2.2
7409 *
7410 * @param string $slug_or_name Old theme's slug or name.
7411 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7412 */
7413 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7414 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7415 $old_theme->get_stylesheet() :
7416 $slug_or_name;
7417
7418 $this->_activate_plugin_event_hook();
7419 }
7420
7421 /**
7422 * Plugin activated hook.
7423 *
7424 * @author Vova Feldman (@svovaf)
7425 * @since 1.0.1
7426 *
7427 * @uses FS_Api
7428 */
7429 function _activate_plugin_event_hook() {
7430 $this->_logger->entrance( 'slug = ' . $this->_slug );
7431
7432 if ( ! $this->is_user_admin() ) {
7433 return;
7434 }
7435
7436 $this->unregister_uninstall_hook();
7437
7438 // Clear API cache on activation.
7439 FS_Api::clear_cache();
7440
7441 $is_premium_version_activation = $this->is_plugin() ?
7442 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7443 $this->is_premium();
7444
7445 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7446 $this->clear_pending_activation_mode();
7447 }
7448
7449 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7450
7451 if ( $this->is_plugin() ) {
7452 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7453 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7454 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7455 $other_version_basename = $is_premium_version_activation ?
7456 $this->_free_plugin_basename :
7457 $this->premium_plugin_basename();
7458
7459 if ( ! $this->_is_network_active ) {
7460 /**
7461 * Themes are always network activated, but the ACTUAL activation is per site.
7462 *
7463 * During the activation, the plugin isn't yet active, therefore,
7464 * _is_network_active will be set to false even if it's a network level
7465 * activation. So we need to fix that by looking at the is_network_admin() value.
7466 *
7467 * @author Vova Feldman
7468 */
7469 $this->_is_network_active = (
7470 $this->_is_multisite_integrated &&
7471 fs_is_network_admin()
7472 );
7473 }
7474
7475 /**
7476 * If the other module version is active, deactivate it.
7477 *
7478 * is_plugin_active() checks if the plugin is active on the site or the network level and
7479 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7480 *
7481 * @author Leo Fajardo (@leorw)
7482 * @since 1.2.2
7483 */
7484 if (
7485 is_plugin_active( $other_version_basename ) &&
7486 $this->apply_filters( 'deactivate_on_activation', ! $this->is_parallel_activation() )
7487 ) {
7488 deactivate_plugins( $other_version_basename );
7489 }
7490 }
7491
7492 if ( $this->is_registered() ) {
7493 if ( $is_premium_version_activation ) {
7494 $this->reconnect_locally();
7495 }
7496
7497
7498 // Schedule re-activation event and sync.
7499 // $this->sync_install( array(), true );
7500 $this->maybe_schedule_install_sync_cron();
7501
7502 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7503 if ( $is_premium_version_activation ) {
7504 $this->_admin_notices->add(
7505 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7506 $this->get_text_x_inline( 'W00t',
7507 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7508 );
7509 }
7510 } else if ( $this->is_anonymous() ) {
7511 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7512 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7513 $network = true;
7514 } else {
7515 $plugin_version = isset( $this->_storage->is_anonymous ) ?
7516 $this->_storage->is_anonymous['version'] :
7517 null;
7518 $network = false;
7519 }
7520
7521 /**
7522 * Reset "skipped" click cache on the following:
7523 * 1. Freemius DEV mode.
7524 * 2. WordPress DEBUG mode.
7525 * 3. If a plugin and the user skipped the exact same version before.
7526 *
7527 * @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).
7528 *
7529 * @todo 4. If explicitly asked to retry after every activation.
7530 */
7531 if ( WP_FS__DEV_MODE ||
7532 (
7533 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7534 $this->get_plugin_version() == $plugin_version
7535 )
7536 ) {
7537 $this->reset_anonymous_mode( $network );
7538 }
7539 }
7540
7541 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7542
7543 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7544 /**
7545 * When activating an add-on, try to also activate a license.
7546 *
7547 * @author Leo Fajardo (@leorw)
7548 * @since 2.3.0
7549 */
7550 if ( ! $this->_is_network_active ) {
7551 $this->maybe_activate_addon_license();
7552 } else {
7553 $this->maybe_network_activate_addon_license();
7554 }
7555
7556 /**
7557 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7558 *
7559 * @author Leo Fajardo (@leorw)
7560 * @since 2.3.0
7561 */
7562 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7563
7564 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7565 $this->_storage->require_license_activation = false;
7566 }
7567 }
7568
7569 if (
7570 $is_premium_version_activation &&
7571 (
7572 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7573 (
7574 $this->is_registered() &&
7575 ! $is_trial_or_has_features_enabled_license
7576 )
7577 )
7578 ) {
7579 $this->_storage->require_license_activation = true;
7580 }
7581
7582 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7583 /**
7584 * If no previous version of plugin's version exist, it means that it's either
7585 * the first time that the plugin installed on the site, or the plugin was installed
7586 * before but didn't have Freemius integrated.
7587 *
7588 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7589 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7590 * only after immediate activation.
7591 *
7592 * @since 1.1.4
7593 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7594 */
7595 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7596 }
7597
7598 /**
7599 * Also flush when activating the premium version so that even if Freemius was off before, the API
7600 * connectivity test can be run again.
7601 *
7602 * @author Leo Fajardo (@leorw)
7603 * @since 2.2.3.1
7604 */
7605 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
7606
7607 if ( ! $this->_anonymous_mode &&
7608 ( false !== $has_api_connectivity ) &&
7609 ! $this->_isAutoInstall
7610 ) {
7611 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7612 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7613 }
7614
7615 /**
7616 * Activation hook is executed after the plugin's main file is loaded, therefore,
7617 * after the plugin was loaded. The logic is located at activate_plugin()
7618 * ./wp-admin/includes/plugin.php.
7619 *
7620 * @author Vova Feldman (@svovaf)
7621 * @since 1.1.9
7622 */
7623 $this->_storage->was_plugin_loaded = true;
7624 }
7625
7626 /**
7627 * @author Leo Fajardo (@leorw)
7628 * @since 2.3.0
7629 */
7630 private function maybe_activate_addon_license() {
7631 $parent_fs = $this->get_parent_instance();
7632
7633 if (
7634 ! is_object( $parent_fs ) ||
7635 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7636 ) {
7637 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7638 return;
7639 }
7640
7641 $license = $this->get_active_parent_license();
7642 if ( ! is_object( $license ) ) {
7643 return;
7644 }
7645
7646 if (
7647 $this->is_bundle_license_auto_activation_enabled() &&
7648 ! empty( $license->products )
7649 ) {
7650 $this->activate_bundle_license( $license );
7651
7652 return;
7653 }
7654
7655 if ( ! $this->is_registered() ) {
7656 // Opt in with a license key.
7657 $this->opt_in(
7658 $parent_fs->get_current_or_network_user()->email,
7659 false,
7660 false,
7661 $license->secret_key,
7662 false,
7663 false,
7664 false,
7665 null,
7666 array(),
7667 true,
7668 $license->user_id
7669 );
7670 } else {
7671 // Activate the license.
7672 $install = $this->api_site_call(
7673 '/',
7674 'put',
7675 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7676 );
7677
7678 if ( ! FS_Api::is_api_error( $install ) ) {
7679 $this->_sync_addon_license( $this->get_id(), true );
7680 }
7681 }
7682 }
7683
7684 /**
7685 * @author Leo Fajardo (@leorw)
7686 * @since 2.3.0
7687 *
7688 * @param FS_Plugin_License $license
7689 */
7690 private function maybe_network_activate_addon_license( $license = null ) {
7691 $parent_fs = $this->get_parent_instance();
7692 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7693 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7694 return;
7695 }
7696
7697 $license = ( ! is_null( $license ) ) ?
7698 $license :
7699 $this->get_active_parent_license();
7700
7701 if ( ! is_object( $license ) ) {
7702 return;
7703 }
7704
7705 if (
7706 $this->is_bundle_license_auto_activation_enabled() &&
7707 ! empty( $license->products )
7708 ) {
7709 $this->activate_bundle_license( $license );
7710
7711 return;
7712 }
7713
7714 if ( ! $this->is_network_registered() ) {
7715 $sites = $this->get_sites_for_network_level_optin();
7716
7717 if ( count( $sites ) > $license->left() ) {
7718 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7719 return;
7720 }
7721
7722 // Opt in with a license key.
7723 $this->opt_in(
7724 $parent_fs->get_user()->email,
7725 false,
7726 false,
7727 $license->secret_key,
7728 false,
7729 false,
7730 false,
7731 null,
7732 $sites,
7733 true,
7734 $license->user_id
7735 );
7736 } else {
7737 $blog_2_install_map = array();
7738 $site_ids = array();
7739
7740 $all_sites = Freemius::get_sites();
7741
7742 foreach ( $all_sites as $site ) {
7743 $blog_id = Freemius::get_site_blog_id( $site );
7744 $install = $this->get_install_by_blog_id( $blog_id );
7745
7746 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7747 // Skip license activation for installs that are already associated with a license.
7748 continue;
7749 }
7750
7751 if ( is_object( $install ) ) {
7752 $blog_2_install_map[ $blog_id ] = $install;
7753 } else {
7754 $site_ids[] = $blog_id;
7755 }
7756 }
7757
7758 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7759 return;
7760 }
7761
7762 $user = $this->get_current_or_network_user();
7763
7764 if ( ! empty( $blog_2_install_map ) ) {
7765 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7766
7767 if ( true !== $result ) {
7768 return;
7769 }
7770 }
7771
7772 if ( ! empty( $site_ids ) ) {
7773 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7774 }
7775 }
7776 }
7777
7778 /**
7779 * Tries to activate a bundle license for all supported products if the current product is activated with a bundle license. This is called after activating an available license (not via the license activation dialog but by clicking on a license activation button) for a product via its "Account" page.
7780 *
7781 * @author Leo Fajardo (@leorw)
7782 * @since 2.4.0
7783 *
7784 * @param FS_Plugin_License $license
7785 * @param array $sites
7786 * @param int $blog_id
7787 */
7788 private function maybe_activate_bundle_license( $license = null, $sites = array(), $blog_id = 0 ) {
7789 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
7790 $license = $this->_license;
7791 }
7792
7793 if ( ! is_object( $license ) ) {
7794 return;
7795 }
7796
7797 $parent_license = ( ! empty( $license->products ) ) ?
7798 $license :
7799 $this->get_active_parent_license( $license->secret_key );
7800
7801 if ( is_object( $parent_license ) ) {
7802 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
7803 }
7804 }
7805
7806 /**
7807 * Try to activate a bundle license for all the bundle products installed on the site.
7808 * (1) If a child product install already has a license, the bundle license won't be activated.
7809 * (2) On multi-site networks, if the attempt to activate the bundle license is triggered from the network admin, the bundle license activation will only work for non-delegated sites and only if none of them is associated with a license. Even if one of the sites has the product installed with a license key, skip the bundle license activation for the product.
7810 * (3) On multi-site networks, if the attempt to activate the bundle license is triggered from a site-level admin, only activate the license if the product is site-level activated or delegated, and the product installation is not yet associated with a license.
7811 *
7812 * @author Leo Fajardo (@leorw)
7813 * @since 2.4.0
7814 *
7815 * @param FS_Plugin_License $license
7816 * @param array $sites
7817 * @param int $current_blog_id
7818 */
7819 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
7820 $is_network_admin = fs_is_network_admin();
7821
7822 $installs_by_blog_map = array();
7823 $site_info_by_blog_map = array();
7824
7825 /**
7826 * Try to activate the license for all supported products.
7827 *
7828 * @author Leo Fajardo
7829 */
7830 foreach ( $license->products as $product_id ) {
7831 $fs = self::get_instance_by_id( $product_id );
7832
7833 if ( ! is_object( $fs ) ) {
7834 continue;
7835 }
7836
7837 if ( ! $fs->has_paid_plan() ) {
7838 continue;
7839 }
7840
7841 if (
7842 ! $fs->is_addon() &&
7843 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
7844 ) {
7845 /**
7846 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
7847 * there is a context bundle.
7848 */
7849 continue;
7850 }
7851
7852 if ( $current_blog_id > 0 ) {
7853 $fs->switch_to_blog( $current_blog_id );
7854 }
7855
7856 if ( $fs->has_active_valid_license() ) {
7857 continue;
7858 }
7859
7860 if ( ! $is_network_admin || $current_blog_id > 0 ) {
7861 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
7862 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
7863 continue;
7864 }
7865 } else {
7866 if ( ! $fs->is_network_active() ) {
7867 // Do not try to activate the license in the network level if the product is not network active.
7868 continue;
7869 }
7870
7871 if ( $fs->is_network_delegated_connection() ) {
7872 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
7873 continue;
7874 }
7875
7876 $has_install_with_license = false;
7877
7878 // Collection of sites that have an install entity that is not activated with a license or non-delegated sites that have no install entity, or both types of site.
7879 $filtered_sites = array();
7880
7881 if ( empty( $sites ) ) {
7882 $all_sites = self::get_sites();
7883
7884 foreach ( $all_sites as $site ) {
7885 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
7886 }
7887 } else {
7888 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
7889 foreach ( $sites as $site ) {
7890 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7891 continue;
7892 }
7893
7894 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
7895 }
7896 }
7897
7898 foreach ( $sites as $site ) {
7899 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
7900 continue;
7901 }
7902
7903 $blog_id = $site['blog_id'];
7904
7905 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
7906 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
7907 }
7908
7909 $installs = $installs_by_blog_map[ $blog_id ];
7910 $install = null;
7911
7912 if ( isset( $installs[ $fs->get_slug() ] ) ) {
7913 $install = $installs[ $fs->get_slug() ];
7914
7915 if (
7916 is_object( $install ) &&
7917 (
7918 ! FS_Site::is_valid_id( $install->id ) ||
7919 ! FS_User::is_valid_id( $install->user_id ) ||
7920 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
7921 )
7922 ) {
7923 $install = null;
7924 }
7925 }
7926
7927 if (
7928 is_object( $install ) &&
7929 FS_Plugin_License::is_valid_id( $install->license_id )
7930 ) {
7931 $has_install_with_license = true;
7932 break;
7933 }
7934
7935 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
7936 // Site activation delegated, don't activate bundle license on the site in the network admin.
7937 continue;
7938 }
7939
7940 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
7941 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
7942 }
7943
7944 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
7945 }
7946
7947 if ( $has_install_with_license || empty( $filtered_sites ) ) {
7948 // Do not try to activate the license at the network level if there's any install with a license or there's no site to activate the license on.
7949 continue;
7950 }
7951
7952 $sites = $filtered_sites;
7953 }
7954
7955 $fs->activate_migrated_license(
7956 $license->secret_key,
7957 null,
7958 null,
7959 $sites,
7960 ( $current_blog_id > 0 ? $current_blog_id : null ),
7961 $license->user_id
7962 );
7963 }
7964 }
7965
7966 /**
7967 * Returns a parent license that can be activated for the context product.
7968 *
7969 * @author Leo Fajardo (@leorw)
7970 * @since 2.3.0
7971 *
7972 * @param string|null $license_key
7973 * @param bool $flush
7974 *
7975 * @return FS_Plugin_License
7976 */
7977 function get_active_parent_license( $license_key = null, $flush = true ) {
7978 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
7979
7980 $fs = $this;
7981
7982 if ( $this->is_addon() ) {
7983 $parent_instance = $this->get_parent_instance();
7984
7985 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
7986 $fs = $parent_instance;
7987 }
7988 }
7989
7990 $foreign_licenses = $fs->get_foreign_licenses_info(
7991 self::get_all_licenses( $this->get_parent_id() )
7992 );
7993
7994 if ( ! empty ( $foreign_licenses ) ) {
7995 $foreign_licenses = array(
7996 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
7997 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
7998 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
7999 );
8000
8001 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8002 }
8003
8004 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8005
8006 if (
8007 ! $this->is_api_result_object( $result, 'licenses' ) ||
8008 ! is_array( $result->licenses ) ||
8009 empty( $result->licenses )
8010 ) {
8011 return null;
8012 }
8013
8014 $parent_license = null;
8015
8016 if ( empty( $license_key ) ) {
8017 $parent_license = $result->licenses[0];
8018 } else {
8019 foreach ( $result->licenses as $license ) {
8020 if ( $license_key === $license->secret_key ) {
8021 $parent_license = $license;
8022 break;
8023 }
8024 }
8025 }
8026
8027 if ( ! is_null( $parent_license ) ) {
8028 $parent_license = new FS_Plugin_License( $parent_license );
8029 }
8030
8031 return $parent_license;
8032 }
8033
8034 /**
8035 * @author Leo Fajardo (@leorw)
8036 * @since 2.3.0
8037 *
8038 * @return array
8039 */
8040 function get_sites_for_network_level_optin() {
8041 $sites = array();
8042 $all_sites = self::get_sites();
8043
8044 foreach ( $all_sites as $site ) {
8045 $blog_id = self::get_site_blog_id( $site );
8046
8047 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8048 ! $this->is_installed_on_site( $blog_id )
8049 ) {
8050 $sites[] = $this->get_site_info( $site );
8051 }
8052 }
8053
8054 return $sites;
8055 }
8056
8057 /**
8058 * Delete account.
8059 *
8060 * @author Vova Feldman (@svovaf)
8061 * @since 1.0.3
8062 *
8063 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8064 */
8065 function delete_account_event( $check_user = true ) {
8066 $this->_logger->entrance( 'slug = ' . $this->_slug );
8067
8068 if ( $check_user && ! $this->is_user_admin() ) {
8069 return;
8070 }
8071
8072 $this->do_action( 'before_account_delete' );
8073
8074 // Clear all admin notices.
8075 $this->_admin_notices->clear_all_sticky( false );
8076
8077 $this->_delete_site( false );
8078
8079 $delete_network_common_data = true;
8080
8081 if ( $this->_is_network_active ) {
8082 $installs = $this->get_blog_install_map();
8083
8084 // Don't delete common network data unless no other installs left.
8085 $delete_network_common_data = empty( $installs );
8086 }
8087
8088 if ( $delete_network_common_data ) {
8089 $this->_delete_plans( false );
8090
8091 $this->_delete_licenses( false );
8092
8093 // Delete add-ons related to plugin's account.
8094 $this->_delete_account_addons( false );
8095 }
8096
8097 // @todo Delete plans and licenses of add-ons.
8098
8099 self::$_accounts->store();
8100
8101 /**
8102 * IMPORTANT:
8103 * Clear crons must be executed before clearing all storage.
8104 * Otherwise, the cron will not be cleared.
8105 */
8106 if ( $delete_network_common_data ) {
8107 $this->clear_sync_cron();
8108 }
8109
8110 $this->clear_install_sync_cron();
8111
8112 // Clear all storage data.
8113 $this->_storage->clear_all( true, array(
8114 'is_delegated_connection',
8115 'connectivity_test',
8116 'is_on',
8117 ), false );
8118
8119 // Send delete event.
8120 $this->get_api_site_scope()->call( '/', 'delete' );
8121
8122 $this->do_action( 'after_account_delete' );
8123 }
8124
8125 /**
8126 * Delete network level account.
8127 *
8128 * @author Vova Feldman (@svovaf)
8129 * @since 2.0.0
8130 *
8131 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8132 */
8133 function delete_network_account_event( $check_user = true ) {
8134 $this->_logger->entrance( 'slug = ' . $this->_slug );
8135
8136 if ( $check_user && ! $this->is_user_admin() ) {
8137 return;
8138 }
8139
8140 $this->do_action( 'before_network_account_delete' );
8141
8142 // Clear all admin notices.
8143 $this->_admin_notices->clear_all_sticky();
8144
8145 $this->_delete_plans( false, false );
8146
8147 $this->_delete_licenses( false );
8148
8149 // Delete add-ons related to plugin's account.
8150 $this->_delete_account_addons( false );
8151
8152 // @todo Delete plans and licenses of add-ons.
8153
8154 self::$_accounts->store( true );
8155
8156 /**
8157 * IMPORTANT:
8158 * Clear crons must be executed before clearing all storage.
8159 * Otherwise, the cron will not be cleared.
8160 */
8161 $this->clear_sync_cron( true );
8162 $this->clear_install_sync_cron( true );
8163
8164 $sites = self::get_sites();
8165
8166 $install_ids = array();
8167 foreach ( $sites as $site ) {
8168 $blog_id = self::get_site_blog_id( $site );
8169
8170 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8171 continue;
8172 }
8173
8174 $install_id = $this->_delete_site( true, $blog_id );
8175
8176 // Clear all storage data.
8177 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8178
8179 if ( FS_Site::is_valid_id( $install_id ) ) {
8180 $install_ids[] = $install_id;
8181 }
8182
8183 switch_to_blog( $blog_id );
8184
8185 $this->do_action( 'after_account_delete' );
8186
8187 restore_current_blog();
8188 }
8189
8190 $this->_storage->clear_all( true, array(
8191 'connectivity_test',
8192 'is_on',
8193 ), true );
8194
8195 // Send delete event.
8196 if ( ! empty( $install_ids ) ) {
8197 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8198 }
8199
8200 $this->do_action( 'after_network_account_delete' );
8201 }
8202
8203 /**
8204 * Plugin deactivation hook.
8205 *
8206 * @author Vova Feldman (@svovaf)
8207 * @since 1.0.1
8208 */
8209 function _deactivate_plugin_hook() {
8210 $this->_logger->entrance( 'slug = ' . $this->_slug );
8211
8212 if ( ! $this->is_user_admin() ) {
8213 return;
8214 }
8215
8216 $is_network_deactivation = fs_is_network_admin();
8217 $storage_keys_for_removal = array();
8218
8219 $this->_admin_notices->clear_all_sticky();
8220
8221 $storage_keys_for_removal[] = 'sticky_optin_added';
8222 if ( isset( $this->_storage->sticky_optin_added ) ) {
8223 unset( $this->_storage->sticky_optin_added );
8224 }
8225
8226 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8227 // Remember that plugin was already installed.
8228 $this->_storage->is_plugin_new_install = false;
8229 }
8230
8231 // Hook to plugin uninstall.
8232 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8233
8234 $this->clear_module_main_file_cache();
8235 $this->clear_sync_cron( $this->_is_network_active );
8236 $this->clear_install_sync_cron();
8237
8238 if ( $this->is_registered() ) {
8239 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8240 FS_Plugin_Updater::instance( $this )->delete_update_data();
8241 }
8242
8243 if ( $is_network_deactivation ) {
8244 // Send deactivation event.
8245 $this->sync_installs( array(
8246 'is_active' => false,
8247 ) );
8248 } else {
8249 // Send deactivation event.
8250 $this->sync_install( array(
8251 'is_active' => false,
8252 ) );
8253 }
8254 } else {
8255 if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
8256 // Reset connectivity test cache.
8257 $this->clear_connectivity_info();
8258
8259 $storage_keys_for_removal[] = 'connectivity_test';
8260 }
8261 }
8262
8263 if ( $is_network_deactivation ) {
8264 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8265 unset( $this->_storage->sticky_optin_added_ms );
8266 }
8267
8268 if ( ! empty( $storage_keys_for_removal ) ) {
8269 $sites = self::get_sites();
8270
8271 foreach ( $sites as $site ) {
8272 $blog_id = self::get_site_blog_id( $site );
8273
8274 foreach ( $storage_keys_for_removal as $key ) {
8275 $this->_storage->remove( $key, false, $blog_id );
8276 }
8277
8278 $this->_storage->save( $blog_id );
8279 }
8280 }
8281 }
8282
8283 // Clear API cache on deactivation.
8284 FS_Api::clear_cache();
8285
8286 $this->remove_sdk_reference();
8287 }
8288
8289 /**
8290 * @author Vova Feldman (@svovaf)
8291 * @since 1.1.6
8292 */
8293 private function remove_sdk_reference() {
8294 global $fs_active_plugins;
8295
8296 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8297 if ( $this->_plugin_basename == $data->plugin_path ) {
8298 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8299 break;
8300 }
8301 }
8302
8303 fs_fallback_to_newest_active_sdk();
8304 }
8305
8306 /**
8307 * @author Vova Feldman (@svovaf)
8308 * @since 1.1.3
8309 *
8310 * @param bool $is_anonymous
8311 * @param bool|int $network_or_blog_id Since 2.0.0
8312 */
8313 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8314 // Store information regarding skip to try and opt-in the user
8315 // again in the future.
8316 $skip_info = array(
8317 'is' => $is_anonymous,
8318 'timestamp' => WP_FS__SCRIPT_START_TIME,
8319 'version' => $this->get_plugin_version(),
8320 );
8321
8322 if ( true === $network_or_blog_id ) {
8323 $this->_storage->is_anonymous_ms = $skip_info;
8324 } else {
8325 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8326 }
8327
8328 $this->network_upgrade_mode_completed();
8329
8330 // Update anonymous mode cache.
8331 $this->_is_anonymous = $is_anonymous;
8332 }
8333
8334 /**
8335 * @author Vova Feldman (@svovaf)
8336 * @since 2.5.1
8337 *
8338 * @param bool|int $network_or_blog_id
8339 */
8340 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8341 if ( true === $network_or_blog_id ) {
8342 unset( $this->_storage->is_anonymous_ms );
8343 } else {
8344 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8345 }
8346 }
8347
8348 /**
8349 * @author Vova Feldman (@svovaf)
8350 * @since 2.0.0
8351 *
8352 * @param int $blog_id Site ID.
8353 * @param int $user_id User ID.
8354 * @param string $domain Site domain.
8355 * @param string $path Site path.
8356 * @param int $network_id Network ID. Only relevant on multi-network installations.
8357 * @param array $meta Metadata. Used to set initial site options.
8358 *
8359 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8360 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8361 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8362 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8363 */
8364 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8365 $this->_logger->entrance();
8366
8367 if ( ! $this->_is_network_active ) {
8368 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8369 return;
8370 }
8371
8372 $site = null;
8373 $new_blog_id = $blog_id;
8374
8375 if ( $this->is_premium() &&
8376 $this->is_network_connected() &&
8377 is_object( $this->_license ) &&
8378 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8379 $this->is_license_network_active( $blog_id )
8380 ) {
8381 /**
8382 * 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.
8383 */
8384 $current_blog_id = get_current_blog_id();
8385 $license = clone $this->_license;
8386
8387 $this->switch_to_blog( $blog_id );
8388
8389 // Opt-in with network user.
8390 $this->install_with_user(
8391 $this->get_network_user(),
8392 $license->secret_key,
8393 false,
8394 false,
8395 false
8396 );
8397
8398 if ( is_object( $this->_site ) ) {
8399 if ( $this->_site->license_id == $license->id ) {
8400 /**
8401 * If the license was activated successfully, sync the license data from the remote server.
8402 */
8403 $this->_license = $license;
8404 $this->sync_site_license();
8405 }
8406 }
8407
8408 $site = $this->_site;
8409
8410 $this->switch_to_blog( $current_blog_id );
8411
8412 if ( is_object( $site ) ) {
8413 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8414
8415 // Already connected (with or without a license), so no need to continue.
8416 return;
8417 }
8418 }
8419
8420 if ( $this->is_network_anonymous() ) {
8421 /**
8422 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8423 */
8424 $this->skip_site_connection( $blog_id );
8425 } else if ( $this->is_network_delegated_connection() ) {
8426 /**
8427 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8428 */
8429 $this->delegate_site_connection( $blog_id );
8430 } else if ( $this->is_network_connected() ) {
8431 /**
8432 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8433 */
8434 $current_blog_id = get_current_blog_id();
8435
8436 $this->switch_to_blog( $blog_id );
8437
8438 // Opt-in with network user.
8439 $this->install_with_user(
8440 $this->get_network_user(),
8441 false,
8442 false,
8443 false,
8444 false
8445 );
8446
8447 $site = $this->_site;
8448
8449 $this->switch_to_blog( $current_blog_id );
8450 } else {
8451 /**
8452 * If the super-admin mixed different options (connect, skip, delegated):
8453 * a) If at least one site connection was delegated, then automatically delegate connection.
8454 * 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.
8455 */
8456 $has_delegated_site = false;
8457
8458 $sites = self::get_sites();
8459 foreach ( $sites as $wp_site ) {
8460 $blog_id = self::get_site_blog_id( $wp_site );
8461
8462 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8463 $has_delegated_site = true;
8464 break;
8465 }
8466 }
8467
8468 if ( $has_delegated_site ) {
8469 $this->delegate_site_connection( $blog_id );
8470 } else {
8471 $this->skip_site_connection( $blog_id );
8472 }
8473 }
8474
8475 /**
8476 * Store the new blog's information even if there's no install so that when a clone install is stored in the new blog's storage, we can try to resolve it automatically.
8477 *
8478 * @author Leo Fajardo (@leorw)
8479 * @since 2.5.0
8480 */
8481 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8482 }
8483
8484 /**
8485 * @author Vova Feldman (@svovaf)
8486 * @since 2.5.0
8487 *
8488 * @param \WP_Site $new_site
8489 * @param array $args
8490 */
8491 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8492 $this->_logger->entrance();
8493
8494 $this->_after_new_blog_callback(
8495 $new_site->id,
8496 // Dummy user ID (not in use).
8497 0,
8498 $new_site->domain,
8499 $new_site->path,
8500 $new_site->network_id,
8501 // Dummy meta, not in use.
8502 array()
8503 );
8504 }
8505
8506 /**
8507 * @author Vova Feldman (@svovaf)
8508 * @since 1.1.3
8509 *
8510 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8511 */
8512 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8513 if ( true === $network_or_blog_ids ) {
8514 $this->unset_anonymous_mode( true );
8515
8516 if ( fs_is_network_admin() ) {
8517 $this->_is_anonymous = null;
8518 }
8519
8520 // Rest anonymous mode for all non-delegated sub-sites.
8521 $blog_ids = $this->get_non_delegated_blog_ids();
8522 }
8523 else
8524 {
8525 if ( false === $network_or_blog_ids ) {
8526 $network_or_blog_ids = 0;
8527 }
8528
8529 $blog_ids = is_array( $network_or_blog_ids ) ?
8530 $network_or_blog_ids :
8531 array( $network_or_blog_ids );
8532
8533 foreach ( $blog_ids as $blog_id ) {
8534 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8535 $this->_is_anonymous = null;
8536 }
8537 }
8538 }
8539
8540 foreach ( $blog_ids as $blog_id ) {
8541 $this->unset_anonymous_mode( $blog_id );
8542 }
8543
8544 /**
8545 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8546 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8547 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8548 *
8549 * @author Leo Fajardo (@leorw)
8550 * @since 1.2.2
8551 */
8552 if ( ! $this->_is_network_active ) {
8553 $this->_is_anonymous = null;
8554 }
8555 }
8556
8557 /**
8558 * @author Leo Fajardo (@leorw)
8559 * @since 2.5.3
8560 */
8561 private function update_license_required_permissions_if_anonymous() {
8562 if ( ! $this->is_anonymous() ) {
8563 return;
8564 }
8565
8566 $this->reset_anonymous_mode( fs_is_network_admin() );
8567
8568 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
8569 'essentials' => true,
8570 'events' => true,
8571 'diagnostic' => false,
8572 'extensions' => false,
8573 'site' => false,
8574 ) );
8575 }
8576
8577 /**
8578 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8579 * deleting the account in the network level, the URL of the page to redirect to is correct.
8580 *
8581 * @author Leo Fajardo (@leorw)
8582 *
8583 * @since 2.1.3
8584 */
8585 private function maybe_set_slug_and_network_menu_exists_flag() {
8586 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8587 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8588 $this->_menu->get_slug() :
8589 $this->_slug
8590 );
8591 }
8592 }
8593
8594 /**
8595 * Clears the anonymous mode and redirects to the opt-in screen.
8596 *
8597 * @author Vova Feldman (@svovaf)
8598 * @since 1.1.7
8599 */
8600 function connect_again() {
8601 if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
8602 return;
8603 }
8604
8605 if ( $this->is_anonymous() ) {
8606 $this->reset_anonymous_mode( fs_is_network_admin() );
8607 }
8608
8609 $activation_url_params = array();
8610
8611 if ( $this->is_pending_activation() ) {
8612 $this->clear_pending_activation_mode();
8613
8614 if ( fs_request_get_bool( 'require_license' ) ) {
8615 $activation_url_params['require_license'] = true;
8616 }
8617 }
8618
8619 $this->maybe_set_slug_and_network_menu_exists_flag();
8620
8621 fs_redirect( $this->get_activation_url( $activation_url_params ) );
8622 }
8623
8624 /**
8625 * Skip account connect, and set anonymous mode.
8626 *
8627 * @author Vova Feldman (@svovaf)
8628 * @since 1.1.1
8629 *
8630 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
8631 */
8632 function skip_connection( $network_or_blog_ids = false ) {
8633 $this->_logger->entrance();
8634
8635 $this->_admin_notices->remove_sticky( 'connect_account' );
8636
8637 if ( true === $network_or_blog_ids ) {
8638 $this->set_anonymous_mode( true, true );
8639
8640 if ( fs_is_network_admin() ) {
8641 $this->_is_anonymous = null;
8642 }
8643
8644 // Rest anonymous mode for all non-delegated sub-sites.
8645 $blog_ids = $this->get_non_delegated_blog_ids();
8646 }
8647 else
8648 {
8649 if ( false === $network_or_blog_ids ) {
8650 $network_or_blog_ids = 0;
8651 }
8652
8653 $blog_ids = is_array( $network_or_blog_ids ) ?
8654 $network_or_blog_ids :
8655 array( $network_or_blog_ids );
8656
8657 foreach ( $blog_ids as $blog_id ) {
8658 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
8659 $this->_is_anonymous = null;
8660 }
8661 }
8662 }
8663
8664 foreach ( $blog_ids as $blog_id ) {
8665 $this->skip_site_connection( $blog_id );
8666 }
8667
8668 $this->network_upgrade_mode_completed();
8669 }
8670
8671 /**
8672 * Skip connection for specific site in the network.
8673 *
8674 * @author Vova Feldman (@svovaf)
8675 * @since 2.0.0
8676 *
8677 * @param int|null $blog_id
8678 * @param bool $send_skip
8679 */
8680 private function skip_site_connection( $blog_id = null ) {
8681 $this->_logger->entrance();
8682
8683 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8684
8685 $this->set_anonymous_mode( true, $blog_id );
8686 }
8687
8688 /**
8689 * Plugin version update hook.
8690 *
8691 * @author Vova Feldman (@svovaf)
8692 * @since 1.0.4
8693 */
8694 private function update_plugin_version_event() {
8695 $this->_logger->entrance();
8696
8697 if ( ! $this->is_registered() ) {
8698 return;
8699 }
8700
8701 $this->maybe_schedule_install_sync_cron();
8702 // $this->sync_install( array(), true );
8703 }
8704
8705 /**
8706 * Generate an MD5 signature of a plugins collection.
8707 * This helper methods used to identify changes in a plugins collection.
8708 *
8709 * @author Vova Feldman (@svovaf)
8710 * @since 2.0.0
8711 *
8712 * @param array [string]array $plugins
8713 *
8714 * @return string
8715 */
8716 private function get_plugins_thumbprint( $plugins ) {
8717 ksort( $plugins );
8718
8719 $thumbprint = '';
8720 foreach ( $plugins as $basename => $data ) {
8721 $thumbprint .= $data['slug'] . ',' .
8722 $data['Version'] . ',' .
8723 ( $data['is_active'] ? '1' : '0' ) . ';';
8724 }
8725
8726 return md5( $thumbprint );
8727 }
8728
8729 /**
8730 * Return a list of modified plugins since the last sync.
8731 *
8732 * Note:
8733 * There's no point to store a plugins counter since even if the number of
8734 * plugins didn't change, we still need to check if the versions are all the
8735 * same and the activity state is similar.
8736 *
8737 * @author Vova Feldman (@svovaf)
8738 * @since 1.1.8
8739 *
8740 * @return array|false
8741 */
8742 private function get_plugins_data_for_api() {
8743 // Alias.
8744 $site_active_plugins_option_name = 'active_plugins';
8745 $network_plugins_option_name = 'all_plugins';
8746
8747 /**
8748 * Collection of all site level active plugins.
8749 */
8750 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8751
8752 if ( ! is_object( $site_active_plugins_cache ) ) {
8753 $site_active_plugins_cache = (object) array(
8754 'timestamp' => '',
8755 'md5' => '',
8756 'plugins' => array(),
8757 );
8758 }
8759
8760 $time = time();
8761
8762 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8763 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8764 ) {
8765 // Don't send plugin updates if last update was in the past 5 min.
8766 return false;
8767 }
8768
8769 // Write timestamp to lock the logic.
8770 $site_active_plugins_cache->timestamp = $time;
8771 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8772
8773 // Reload options from DB.
8774 self::$_accounts->load( true );
8775 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8776
8777 if ( $time != $site_active_plugins_cache->timestamp ) {
8778 // If timestamp is different, then another thread captured the lock.
8779 return false;
8780 }
8781
8782 /**
8783 * Collection of all plugins (network level).
8784 */
8785 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8786
8787 if ( ! is_object( $network_plugins_cache ) ) {
8788 $network_plugins_cache = (object) array(
8789 'timestamp' => '',
8790 'md5' => '',
8791 'plugins' => array(),
8792 );
8793 }
8794
8795 // Check if there's a change in plugins.
8796 $network_plugins = self::get_network_plugins();
8797 $site_active_plugins = self::get_site_active_plugins();
8798
8799 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8800 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8801
8802 // Check if plugins status changed (version or active/inactive).
8803 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8804 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8805
8806 if ( ! $network_plugins_changed &&
8807 ! $site_active_plugins_changed
8808 ) {
8809 // No changes.
8810 return array();
8811 }
8812
8813 $plugins_update_data = array();
8814
8815 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8816 if ( ! isset( $network_plugins[ $basename ] ) ) {
8817 // Plugin uninstalled.
8818 $uninstalled_plugin_data = $data;
8819 $uninstalled_plugin_data['is_active'] = false;
8820 $uninstalled_plugin_data['is_uninstalled'] = true;
8821 $plugins_update_data[] = $uninstalled_plugin_data;
8822
8823 unset( $network_plugins[ $basename ] );
8824
8825 unset( $network_plugins_cache->plugins[ $basename ] );
8826 unset( $site_active_plugins_cache->plugins[ $basename ] );
8827
8828 continue;
8829 }
8830
8831 $was_active = $data['is_active'] ||
8832 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8833 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8834 $is_active = $network_plugins[ $basename ]['is_active'] ||
8835 ( isset( $site_active_plugins[ $basename ] ) &&
8836 $site_active_plugins[ $basename ]['is_active'] );
8837
8838 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8839 isset( $site_active_plugins[ $basename ] )
8840 ) {
8841 // Plugin was site level activated.
8842 $site_active_plugins_cache->plugins[ $basename ] = array(
8843 'slug' => $network_plugins[ $basename ]['slug'],
8844 'version' => $network_plugins[ $basename ]['Version'],
8845 'title' => $network_plugins[ $basename ]['Name'],
8846 'is_active' => $is_active,
8847 'is_uninstalled' => false,
8848 );
8849 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8850 ! isset( $site_active_plugins[ $basename ] )
8851 ) {
8852 // Plugin was site level deactivated.
8853 unset( $site_active_plugins_cache->plugins[ $basename ] );
8854 }
8855
8856 $prev_version = $data['version'];
8857 $current_version = $network_plugins[ $basename ]['Version'];
8858
8859 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8860 // Plugin activated or deactivated, or version changed.
8861
8862 if ( $was_active !== $is_active ) {
8863 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8864 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8865 }
8866 }
8867
8868 if ( $prev_version !== $current_version ) {
8869 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8870 }
8871
8872 $updated_plugin_data = $data;
8873 $updated_plugin_data['is_active'] = $is_active;
8874 $updated_plugin_data['version'] = $current_version;
8875 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8876 $plugins_update_data[] = $updated_plugin_data;
8877 }
8878 }
8879
8880 // Find new plugins that weren't yet seen before.
8881 foreach ( $network_plugins as $basename => $data ) {
8882 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8883 // New plugin.
8884 $new_plugin = array(
8885 'slug' => $data['slug'],
8886 'version' => $data['Version'],
8887 'title' => $data['Name'],
8888 'is_active' => $data['is_active'],
8889 'is_uninstalled' => false,
8890 );
8891
8892 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8893
8894 $is_site_level_active = (
8895 isset( $site_active_plugins[ $basename ] ) &&
8896 $site_active_plugins[ $basename ]['is_active']
8897 );
8898
8899 /**
8900 * If not network active, set the activity status based on the site-level plugin status.
8901 */
8902 if ( ! $new_plugin['is_active'] ) {
8903 $new_plugin['is_active'] = $is_site_level_active;
8904 }
8905
8906 $plugins_update_data[] = $new_plugin;
8907
8908 if ( isset( $site_active_plugins[ $basename ] ) ) {
8909 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8910 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
8911 }
8912 }
8913 }
8914
8915 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8916 $site_active_plugins_cache->timestamp = $time;
8917 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8918
8919 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8920 $network_plugins_cache->timestamp = $time;
8921 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8922
8923 return $plugins_update_data;
8924 }
8925
8926 /**
8927 * Return a list of modified themes since the last sync.
8928 *
8929 * Note:
8930 * There's no point to store a themes counter since even if the number of
8931 * themes didn't change, we still need to check if the versions are all the
8932 * same and the activity state is similar.
8933 *
8934 * @author Vova Feldman (@svovaf)
8935 * @since 1.1.8
8936 *
8937 * @return array|false
8938 */
8939 private function get_themes_data_for_api() {
8940 // Alias.
8941 $option_name = 'all_themes';
8942
8943 $all_cached_themes = self::$_accounts->get_option( $option_name );
8944
8945 if ( ! is_object( $all_cached_themes ) ) {
8946 $all_cached_themes = (object) array(
8947 'timestamp' => '',
8948 'md5' => '',
8949 'themes' => array(),
8950 );
8951 }
8952
8953 $time = time();
8954
8955 if ( ! empty( $all_cached_themes->timestamp ) &&
8956 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8957 ) {
8958 // Don't send theme updates if last update was in the past 5 min.
8959 return false;
8960 }
8961
8962 // Write timestamp to lock the logic.
8963 $all_cached_themes->timestamp = $time;
8964 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8965
8966 // Reload options from DB.
8967 self::$_accounts->load( true );
8968 $all_cached_themes = self::$_accounts->get_option( $option_name );
8969
8970 if ( $time != $all_cached_themes->timestamp ) {
8971 // If timestamp is different, then another thread captured the lock.
8972 return false;
8973 }
8974
8975 // Get active theme.
8976 $active_theme = wp_get_theme();
8977 $active_theme_stylesheet = $active_theme->get_stylesheet();
8978
8979 // Check if there's a change in themes.
8980 $all_themes = wp_get_themes();
8981
8982 // Check if themes changed.
8983 ksort( $all_themes );
8984
8985 $themes_signature = '';
8986 foreach ( $all_themes as $slug => $data ) {
8987 $is_active = ( $slug === $active_theme_stylesheet );
8988 $themes_signature .= $slug . ',' .
8989 $data->version . ',' .
8990 ( $is_active ? '1' : '0' ) . ';';
8991 }
8992
8993 // Check if themes status changed (version or active/inactive).
8994 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8995
8996 $themes_update_data = array();
8997
8998 if ( $themes_changed ) {
8999 // Change in themes, report changes.
9000
9001 // Update existing themes info.
9002 foreach ( $all_cached_themes->themes as $slug => $data ) {
9003 $is_active = ( $slug === $active_theme_stylesheet );
9004
9005 if ( ! isset( $all_themes[ $slug ] ) ) {
9006 // Plugin uninstalled.
9007 $uninstalled_theme_data = $data;
9008 $uninstalled_theme_data['is_active'] = false;
9009 $uninstalled_theme_data['is_uninstalled'] = true;
9010 $themes_update_data[] = $uninstalled_theme_data;
9011
9012 unset( $all_themes[ $slug ] );
9013 unset( $all_cached_themes->themes[ $slug ] );
9014 } else if ( $data['is_active'] !== $is_active ||
9015 $data['version'] !== $all_themes[ $slug ]->version
9016 ) {
9017 // Plugin activated or deactivated, or version changed.
9018
9019 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9020 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9021
9022 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9023 }
9024 }
9025
9026 // Find new themes that weren't yet seen before.
9027 foreach ( $all_themes as $slug => $data ) {
9028 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9029 $is_active = ( $slug === $active_theme_stylesheet );
9030
9031 // New plugin.
9032 $new_plugin = array(
9033 'slug' => $slug,
9034 'version' => $data->version,
9035 'title' => $data->name,
9036 'is_active' => $is_active,
9037 'is_uninstalled' => false,
9038 );
9039
9040 $themes_update_data[] = $new_plugin;
9041 $all_cached_themes->themes[ $slug ] = $new_plugin;
9042 }
9043 }
9044
9045 $all_cached_themes->md5 = md5( $themes_signature );
9046 $all_cached_themes->timestamp = time();
9047 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9048 }
9049
9050 return $themes_update_data;
9051 }
9052
9053 /**
9054 * Get site data for API install request.
9055 *
9056 * @author Vova Feldman (@svovaf)
9057 * @since 1.1.2
9058 *
9059 * @param string[] $override
9060 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9061 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9062 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9063 *
9064 * @return array
9065 */
9066 private function get_install_data_for_api(
9067 array $override,
9068 $include_plugins = true,
9069 $include_themes = true,
9070 $include_blog_data = true
9071 ) {
9072 // Alias.
9073 $permissions = FS_Permission_Manager::instance( $this );
9074
9075 if ( $permissions->is_extensions_tracking_allowed() ) {
9076 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9077 /**
9078 * @since 1.1.8 Also send plugin updates.
9079 */
9080 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9081 $plugins = $this->get_plugins_data_for_api();
9082 if ( ! empty( $plugins ) ) {
9083 $override['plugins'] = $plugins;
9084 }
9085 }
9086 }
9087
9088 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9089 /**
9090 * @since 1.1.8 Also send themes updates.
9091 */
9092 if ( $include_themes && ! isset( $override['themes'] ) ) {
9093 $themes = $this->get_themes_data_for_api();
9094 if ( ! empty( $themes ) ) {
9095 $override['themes'] = $themes;
9096 }
9097 }
9098 }
9099 }
9100
9101 $versions = $this->get_versions();
9102
9103 $blog_data = array();
9104 if ( $include_blog_data ) {
9105 $blog_data['url'] = self::get_unfiltered_site_url();
9106
9107 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9108 $blog_data = array_merge( $blog_data, array(
9109 'language' => self::get_sanitized_language(),
9110 'title' => get_bloginfo( 'name' ),
9111 ) );
9112 }
9113 }
9114
9115 return array_merge( $versions, $blog_data, array(
9116 'version' => $this->get_plugin_version(),
9117 'is_premium' => $this->is_premium(),
9118 // Special params.
9119 'is_active' => true,
9120 'is_uninstalled' => false,
9121 ), $override );
9122 }
9123
9124 /**
9125 * Update installs details.
9126 *
9127 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9128 *
9129 * @author Vova Feldman (@svovaf)
9130 * @since 2.0.0
9131 *
9132 * @param string[] string $override
9133 * @param bool $only_diff
9134 * @param bool $is_keepalive
9135 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9136 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9137 *
9138 * @return array
9139 */
9140 private function get_installs_data_for_api(
9141 array $override,
9142 $only_diff = false,
9143 $is_keepalive = false,
9144 $include_plugins = true,
9145 $include_themes = true
9146 ) {
9147 /**
9148 * @since 1.1.8 Also send plugin updates.
9149 */
9150 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9151 // $plugins = $this->get_plugins_data_for_api();
9152 // if ( ! empty( $plugins ) ) {
9153 // $override['plugins'] = $plugins;
9154 // }
9155 // }
9156 /**
9157 * @since 1.1.8 Also send themes updates.
9158 */
9159 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9160 // $themes = $this->get_themes_data_for_api();
9161 // if ( ! empty( $themes ) ) {
9162 // $override['themes'] = $themes;
9163 // }
9164 // }
9165
9166 // Common properties.
9167 $versions = $this->get_versions();
9168 $common = array_merge( $versions, array(
9169 'version' => $this->get_plugin_version(),
9170 'is_premium' => $this->is_premium(),
9171 ), $override );
9172
9173
9174 $is_common_diff_for_any_site = false;
9175 $common_diff_union = array();
9176
9177 $installs_data = array();
9178
9179 $sites = self::get_sites();
9180
9181 $subsite_data_for_api_by_install_id = array();
9182 $install_url_by_install_id = array();
9183 $subsite_registration_date_by_install_id = array();
9184
9185 foreach ( $sites as $site ) {
9186 $blog_id = self::get_site_blog_id( $site );
9187
9188 $install = $this->get_install_by_blog_id( $blog_id );
9189
9190 if ( is_object( $install ) ) {
9191 if ( $install->user_id != $this->_user->id ) {
9192 // Install belongs to a different owner.
9193 continue;
9194 }
9195
9196 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9197 // Don't send updates regarding opted-out installs.
9198 continue;
9199 }
9200
9201 $install_data = $this->get_site_info( $site, true );
9202
9203 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9204 continue;
9205 }
9206
9207 $uid = $install_data['uid'];
9208 $url = $install_data['url'];
9209 $registration_date = $install_data['registration_date'];
9210
9211 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9212 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9213 $clone_install_url = $install_url_by_install_id[ $install->id ];
9214 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9215
9216 $skip = false;
9217
9218 if (
9219 ! empty( $install_data['registration_date'] ) &&
9220 ! empty( $clone_subsite_registration_date )
9221 ) {
9222 /**
9223 * If the current subsite was created after the other subsite that is also linked to the same install ID, we assume that it's a clone (not the original), and therefore, would skip its processing.
9224 *
9225 * @author Leo Fajardo (@leorw)
9226 * @since 2.5.1
9227 */
9228 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9229 } else if (
9230 /**
9231 * If we already have an install with the same URL as the subsite it's stored in, skip the current subsite. Otherwise, replace the existing install's data with the current subsite's install's data if the URLs match.
9232 *
9233 * @author Leo Fajardo (@leorw)
9234 * @since 2.5.0
9235 */
9236 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9237 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9238 ) {
9239 $skip = true;
9240 }
9241
9242 if ( $skip ) {
9243 // Store the skipped subsite's ID so that the clone resolution manager can try to resolve the clone install that is stored in that subsite later on.
9244 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9245 continue;
9246 }
9247 }
9248
9249 unset( $install_data['blog_id'] );
9250 unset( $install_data['uid'] );
9251 unset( $install_data['url'] );
9252 unset( $install_data['registration_date'] );
9253
9254 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9255 $install_data['is_uninstalled'] = $install->is_uninstalled;
9256
9257 $common_diff = null;
9258 $is_common_diff = false;
9259 if ( $only_diff ) {
9260 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9261 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9262
9263 $is_common_diff = ! empty( $common_diff );
9264
9265 if ( $is_common_diff ) {
9266 foreach ( $common_diff as $k => $v ) {
9267 if ( ! isset( $common_diff_union[ $k ] ) ) {
9268 $common_diff_union[ $k ] = $v;
9269 }
9270 }
9271 }
9272
9273 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9274 }
9275
9276 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9277 // Add install ID and site unique ID.
9278 $install_data['id'] = $install->id;
9279 $install_data['uid'] = $uid;
9280 $install_data['url'] = $url;
9281
9282 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9283 $install_url_by_install_id[ $install->id ] = $install->url;
9284 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9285 }
9286 }
9287 }
9288
9289 restore_current_blog();
9290
9291 $installs_data = array_merge(
9292 $installs_data,
9293 array_values( $subsite_data_for_api_by_install_id )
9294 );
9295
9296 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9297 if ( ! $only_diff ) {
9298 $installs_data[] = $common;
9299 } else if ( ! empty( $common_diff_union ) ) {
9300 $installs_data[] = $common_diff_union;
9301 }
9302 }
9303
9304 foreach ( $installs_data as &$data ) {
9305 $data = (object) $data;
9306 }
9307
9308 return $installs_data;
9309 }
9310
9311 /**
9312 * Compare site actual data to the stored install data and return the differences for an API data sync.
9313 *
9314 * @author Vova Feldman (@svovaf)
9315 * @since 2.0.0
9316 *
9317 * @param array $site
9318 * @param FS_Site $install
9319 * @param string[] string $override
9320 *
9321 * @return array
9322 */
9323 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9324 $diff = array();
9325 $special = array();
9326 $special_override = false;
9327
9328 foreach ( $site as $p => $v ) {
9329 if ( property_exists( $install, $p ) ) {
9330 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9331 $install->{$p} != $v
9332 ) {
9333 $val = self::get_api_sanitized_property( $p, $v );
9334
9335 if ( $install->{$p} != $val ) {
9336 $install->{$p} = $val;
9337 $diff[ $p ] = $val;
9338 }
9339 }
9340 } else {
9341 $special[ $p ] = $v;
9342
9343 if ( isset( $override[ $p ] ) ||
9344 'plugins' === $p ||
9345 'themes' === $p
9346 ) {
9347 $special_override = true;
9348 }
9349 }
9350 }
9351
9352 if ( $special_override || 0 < count( $diff ) ) {
9353 // Add special params only if has at least one
9354 // standard param, or if explicitly requested to
9355 // override a special param or a param which is not exist
9356 // in the install object.
9357 $diff = array_merge( $diff, $special );
9358 }
9359
9360 return $diff;
9361 }
9362
9363 /**
9364 * @author Leo Fajardo (@leorw)
9365 * @since 2.5.1
9366 */
9367 private function send_pending_clone_update_once() {
9368 $this->_logger->entrance();
9369
9370 if ( ! empty( $this->_storage->clone_id ) ) {
9371 return;
9372 }
9373
9374 $install_clone = $this->get_api_site_scope()->call(
9375 '/clones',
9376 'post',
9377 array( 'site_url' => self::get_unfiltered_site_url() )
9378 );
9379
9380 if ( $this->is_api_result_entity( $install_clone ) ) {
9381 $this->_storage->clone_id = $install_clone->id;
9382 }
9383 }
9384
9385 /**
9386 * @author Leo Fajardo (@leorw)
9387 * @since 2.5.1
9388 *
9389 * @param string $resolution_type
9390 * @param FS_Site $clone_context_install
9391 */
9392 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9393 $this->_logger->entrance();
9394
9395 if ( empty( $this->_storage->clone_id ) ) {
9396 return;
9397 }
9398
9399 $new_install_id = null;
9400 $current_site = null;
9401
9402 $flush = false;
9403
9404 /**
9405 * If the current site is now different from the context install before the clone resolution, we need to override `$this->_site` so that the API call below will be made with the right install scope entity.
9406 */
9407 if ( $clone_context_install->id != $this->_site->id ) {
9408 $new_install_id = $this->_site->id;
9409 $current_site = $this->_site;
9410 $this->_site = $clone_context_install;
9411
9412 $flush = true;
9413 }
9414
9415 $this->get_api_site_scope( $flush )->call(
9416 "/clones/{$this->_storage->clone_id}",
9417 'put',
9418 array(
9419 'resolution' => $resolution_type,
9420 'new_install_id' => $new_install_id,
9421 )
9422 );
9423
9424 if ( is_object( $current_site ) ) {
9425 /**
9426 * Ensure that the install scope entity is updated back to the previous install entity.
9427 */
9428 $this->_site = $current_site;
9429
9430 // Restore the previous install scope entity of the API.
9431 $this->get_api_site_scope( true );
9432 }
9433 }
9434
9435 /**
9436 * Update install only if changed.
9437 *
9438 * @author Vova Feldman (@svovaf)
9439 * @since 1.0.9
9440 *
9441 * @param string[] string $override
9442 * @param bool $flush
9443 * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
9444 *
9445 * @return false|object|string
9446 */
9447 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9448 $this->_logger->entrance();
9449
9450 $check_properties = $this->get_install_data_for_api( $override );
9451
9452 if ( $flush ) {
9453 $params = $check_properties;
9454 } else {
9455 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9456 }
9457
9458 if ( empty( $params ) ) {
9459 $keepalive_only_update = $this->should_send_keepalive_update();
9460
9461 if ( ! $keepalive_only_update ) {
9462 /**
9463 * There are no updates to send including keepalive.
9464 *
9465 * @author Leo Fajardo (@leorw)
9466 * @since 2.2.3
9467 */
9468 return false;
9469 }
9470 }
9471
9472 if ( $is_two_way_sync ) {
9473 /**
9474 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9475 *
9476 * @author Leo Fajardo (@leorw)
9477 * @since 2.2.3
9478 */
9479 if ( ! is_multisite() ) {
9480 // Update last install sync timestamp.
9481 $this->set_cron_execution_timestamp( 'install_sync' );
9482 }
9483
9484 $params['uid'] = $this->get_anonymous_id();
9485 }
9486
9487 $this->set_keepalive_timestamp();
9488
9489 // Send updated values to FS.
9490 $site = $this->api_site_call( '/', 'put', $params, true );
9491
9492 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9493 /**
9494 * Clear scheduled install sync after a two-way sync call.
9495 *
9496 * @author Leo Fajardo (@leorw)
9497 * @since 2.2.3
9498 */
9499 if ( ! is_multisite() ) {
9500 // I successfully sent install update, clear scheduled sync if exist.
9501 $this->clear_install_sync_cron();
9502 }
9503 }
9504
9505 return $site;
9506 }
9507
9508 /**
9509 * Update installs only if changed.
9510 *
9511 * @author Vova Feldman (@svovaf)
9512 * @since 2.0.0
9513 *
9514 * @param string[] string $override
9515 * @param bool $flush
9516 * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
9517 *
9518 * @return false|object|string
9519 */
9520 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9521 $this->_logger->entrance();
9522
9523 /**
9524 * Pass `true` to use the network level storage since the update is for many installs.
9525 *
9526 * @author Leo Fajardo (@leorw)
9527 * @since 2.2.3
9528 */
9529 $should_send_keepalive = $this->should_send_keepalive_update( true );
9530
9531 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9532
9533 if ( empty( $installs_data ) ) {
9534 return false;
9535 }
9536
9537 if ( $is_two_way_sync ) {
9538 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9539 $this->set_cron_execution_timestamp( 'install_sync' );
9540 }
9541
9542 /**
9543 * Pass `true` to use the network level storage since the update is for many installs.
9544 *
9545 * @author Leo Fajardo (@leorw)
9546 * @since 2.2.3
9547 */
9548 $this->set_keepalive_timestamp( true );
9549
9550 // Send updated values to FS.
9551 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9552
9553 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9554 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9555 $this->clear_install_sync_cron();
9556 }
9557
9558 return $result;
9559 }
9560
9561 /**
9562 * @author Leo Fajardo (@leorw)
9563 *
9564 * @param bool|null $use_network_level_storage
9565 *
9566 * @return bool
9567 */
9568 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9569 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9570
9571 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9572 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9573 return true;
9574 } else {
9575 // 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.
9576 return ( 7 == rand( 1, 7 ) );
9577 }
9578 }
9579
9580 /**
9581 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
9582 *
9583 * @author Leo Fajardo (@leorw)
9584 * @since 2.3.2
9585 */
9586 private function maybe_sync_install_user() {
9587 if ( $this->_user->id == $this->_site->user_id ) {
9588 return;
9589 }
9590
9591 // Fetch user data and store if found.
9592 $this->sync_user_by_current_install();
9593 }
9594
9595 /**
9596 * Update install only if changed.
9597 *
9598 * @author Vova Feldman (@svovaf)
9599 * @since 1.0.9
9600 *
9601 * @param string[] string $override
9602 * @param bool $flush
9603 */
9604 function sync_install( $override = array(), $flush = false ) {
9605 $this->_logger->entrance();
9606
9607 $site = $this->send_install_update( $override, $flush, true );
9608
9609 if ( false === $site ) {
9610 // No sync required.
9611 return;
9612 }
9613
9614 if ( ! $this->is_api_result_entity( $site ) ) {
9615 // Failed to sync, don't update locally.
9616 return;
9617 }
9618
9619 $this->_site = new FS_Site( $site );
9620
9621 $this->_store_site( true );
9622 }
9623
9624 /**
9625 * Update install only if changed.
9626 *
9627 * @author Vova Feldman (@svovaf)
9628 * @since 1.0.9
9629 *
9630 * @param string[] string $override
9631 * @param bool $flush
9632 */
9633 private function sync_installs( $override = array(), $flush = false ) {
9634 $this->_logger->entrance();
9635
9636 $result = $this->send_installs_update( $override, $flush, true );
9637
9638 if ( false === $result ) {
9639 // No sync required.
9640 return;
9641 }
9642
9643 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9644 // Failed to sync, don't update locally.
9645 return;
9646 }
9647
9648 $address_to_blog_map = $this->get_address_to_blog_map();
9649
9650 foreach ( $result->installs as $install ) {
9651 $this->_site = new FS_Site( $install );
9652
9653 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9654 $blog_id = $address_to_blog_map[ $address ];
9655
9656 $this->_store_site( true, $blog_id );
9657 }
9658 }
9659
9660 /**
9661 * Track install's custom event.
9662 *
9663 * IMPORTANT:
9664 * Custom event tracking is currently only supported for specific clients.
9665 * If you are not one of them, please don't use this method. If you will,
9666 * the API will simply ignore your request based on the plugin ID.
9667 *
9668 * Need custom tracking for your plugin or theme?
9669 * If you are interested in custom event tracking please contact yo@freemius.com
9670 * for further details.
9671 *
9672 * @author Vova Feldman (@svovaf)
9673 * @since 1.2.1
9674 *
9675 * @param string $name Event name.
9676 * @param array $properties Associative key/value array with primitive values only
9677 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9678 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9679 *
9680 * @return object|false Event data or FALSE on failure.
9681 *
9682 * @throws \Freemius_InvalidArgumentException
9683 */
9684 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9685 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9686
9687 if ( ! $this->is_registered() ) {
9688 return false;
9689 }
9690
9691 $event = array( 'type' => $name );
9692
9693 if ( is_numeric( $process_at ) && $process_at > time() ) {
9694 $event['process_at'] = $process_at;
9695 }
9696
9697 if ( $once ) {
9698 $event['once'] = true;
9699 }
9700
9701 if ( ! empty( $properties ) ) {
9702 // Verify associative array values are primitive.
9703 foreach ( $properties as $k => $v ) {
9704 if ( ! is_scalar( $v ) ) {
9705 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9706 }
9707 }
9708
9709 $event['properties'] = $properties;
9710 }
9711
9712 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9713
9714 return $this->is_api_error( $result ) ?
9715 false :
9716 $result;
9717 }
9718
9719 /**
9720 * Track install's custom event only once, but it still triggers the API call.
9721 *
9722 * IMPORTANT:
9723 * Custom event tracking is currently only supported for specific clients.
9724 * If you are not one of them, please don't use this method. If you will,
9725 * the API will simply ignore your request based on the plugin ID.
9726 *
9727 * Need custom tracking for your plugin or theme?
9728 * If you are interested in custom event tracking please contact yo@freemius.com
9729 * for further details.
9730 *
9731 * @author Vova Feldman (@svovaf)
9732 * @since 1.2.1
9733 *
9734 * @param string $name Event name.
9735 * @param array $properties Associative key/value array with primitive values only
9736 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9737 *
9738 * @return object|false Event data or FALSE on failure.
9739 *
9740 * @throws \Freemius_InvalidArgumentException
9741 *
9742 * @user Freemius::track_event()
9743 */
9744 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9745 return $this->track_event( $name, $properties, $process_at, true );
9746 }
9747
9748 /**
9749 * Plugin uninstall hook.
9750 *
9751 * @author Vova Feldman (@svovaf)
9752 * @since 1.0.1
9753 *
9754 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9755 */
9756 function _uninstall_plugin_event( $check_user = true ) {
9757 $this->_logger->entrance( 'slug = ' . $this->_slug );
9758
9759 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9760 return;
9761 }
9762
9763 $params = array();
9764 $uninstall_reason = null;
9765 if ( isset( $this->_storage->uninstall_reason ) ) {
9766 $uninstall_reason = $this->_storage->uninstall_reason;
9767 $params['reason_id'] = $uninstall_reason->id;
9768 $params['reason_info'] = $uninstall_reason->info;
9769 }
9770
9771 if ( ! $this->is_registered() ) {
9772 // Send anonymous uninstall event only if user submitted a feedback.
9773 if ( isset( $uninstall_reason ) ) {
9774 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9775 $this->opt_in( false, false, false, false, true );
9776 } else {
9777 $params['uid'] = $this->get_anonymous_id();
9778 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9779 }
9780 }
9781 } else {
9782 $params = array_merge( $params, array(
9783 'is_active' => false,
9784 'is_uninstalled' => true,
9785 ) );
9786
9787 if ( $this->_is_network_active ) {
9788 // Send uninstall event.
9789 $this->send_installs_update( $params );
9790 } else {
9791 // Send uninstall event and handle the result.
9792 $this->sync_install( $params );
9793 }
9794 }
9795
9796 // @todo Decide if we want to delete plugin information from db.
9797 }
9798
9799 /**
9800 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9801 *
9802 * @author Vova Feldman (@svovaf)
9803 * @since 2.2.1
9804 *
9805 * @param string $is_premium
9806 * @param string $caller
9807 *
9808 * @return void
9809 */
9810 function set_basename( $is_premium, $caller ) {
9811 $basename = plugin_basename( $caller );
9812
9813 $current_basename = $is_premium ?
9814 $this->_premium_plugin_basename :
9815 $this->_free_plugin_basename;
9816
9817 if ( $current_basename == $basename ) {
9818 // Basename value set correctly.
9819 return;
9820 }
9821
9822 if ( $is_premium ) {
9823 $this->_premium_plugin_basename = $basename;
9824 } else {
9825 $this->_free_plugin_basename = $basename;
9826 }
9827
9828 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9829
9830 register_activation_hook(
9831 $plugin_dir . $basename,
9832 array( &$this, '_activate_plugin_event_hook' )
9833 );
9834 }
9835
9836 /**
9837 * @author Vova Feldman (@svovaf)
9838 * @since 1.1.1
9839 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9840 *
9841 * @return string
9842 */
9843 function premium_plugin_basename() {
9844 if ( ! isset( $this->_premium_plugin_basename ) ) {
9845 $this->_premium_plugin_basename = $this->is_premium() ?
9846 // The product is premium, so use the current basename.
9847 $this->_plugin_basename :
9848 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9849 }
9850
9851 return $this->_premium_plugin_basename;
9852 }
9853
9854 /**
9855 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9856 *
9857 * @author Vova Feldman (@svovaf)
9858 * @since 1.0.2
9859 */
9860 public static function _uninstall_plugin_hook() {
9861 self::_load_required_static();
9862
9863 self::$_static_logger->entrance();
9864
9865 if ( ! current_user_can( 'activate_plugins' ) ) {
9866 return;
9867 }
9868
9869 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9870
9871 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9872
9873 define( 'WP_FS__UNINSTALL_MODE', true );
9874
9875 $fs = self::get_instance_by_file( $plugin_file );
9876
9877 if ( is_object( $fs ) ) {
9878 $fs->remove_sdk_reference();
9879
9880 self::require_plugin_essentials();
9881
9882 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9883 is_plugin_active( $fs->premium_plugin_basename() )
9884 ) {
9885 // Deleting Free or Premium plugin version while the other version still installed.
9886 return;
9887 }
9888
9889 if (
9890 ! $fs->is_clone() &&
9891 /**
9892 * If there's a context install, run this method only when there's also a context user (e.g., when cloning a subsite of a multisite network into a single-site installation, it's possible for an install to be associated with a non-existing user entity; we want Freemius to be off in this case, while we are trying to recover the user).
9893 *
9894 * @author Leo Fajardo
9895 */
9896 ( ! is_object( $fs->_site ) || $fs->is_registered() )
9897 ) {
9898 $fs->_uninstall_plugin_event();
9899 }
9900
9901 $fs->do_action( 'after_uninstall' );
9902 }
9903 }
9904
9905 #----------------------------------------------------------------------------------
9906 #region Plugin Information
9907 #----------------------------------------------------------------------------------
9908
9909 /**
9910 * Load WordPress core plugin.php essential module.
9911 *
9912 * @author Vova Feldman (@svovaf)
9913 * @since 1.1.1
9914 */
9915 private static function require_plugin_essentials() {
9916 if ( ! function_exists( 'get_plugins' ) ) {
9917 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9918
9919 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9920 }
9921 }
9922
9923 /**
9924 * Load WordPress core pluggable.php module.
9925 *
9926 * @author Vova Feldman (@svovaf)
9927 * @since 1.1.2
9928 */
9929 private static function require_pluggable_essentials() {
9930 if ( ! function_exists( 'wp_get_current_user' ) ) {
9931 require_once ABSPATH . 'wp-includes/pluggable.php';
9932 }
9933 }
9934
9935 /**
9936 * Return plugin data.
9937 *
9938 * @author Vova Feldman (@svovaf)
9939 * @since 1.0.1
9940 *
9941 * @param bool $reparse_plugin_metadata
9942 *
9943 * @return array
9944 */
9945 function get_plugin_data( $reparse_plugin_metadata = false ) {
9946 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
9947 self::require_plugin_essentials();
9948
9949 if ( $this->is_plugin() ) {
9950 /**
9951 * @author Vova Feldman (@svovaf)
9952 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9953 *
9954 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9955 */
9956 $plugin_data = get_plugin_data(
9957 $this->_plugin_main_file_path,
9958 false,
9959 false
9960 );
9961 } else {
9962 $theme_data = wp_get_theme();
9963
9964 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9965 $parent_theme = $theme_data->parent();
9966
9967 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9968 $theme_data = $parent_theme;
9969 }
9970 }
9971
9972 $plugin_data = array(
9973 'Name' => $theme_data->get( 'Name' ),
9974 'Version' => $theme_data->get( 'Version' ),
9975 'Author' => $theme_data->get( 'Author' ),
9976 'Description' => $theme_data->get( 'Description' ),
9977 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9978 );
9979 }
9980
9981 $this->_plugin_data = $plugin_data;
9982 }
9983
9984 return $this->_plugin_data;
9985 }
9986
9987 /**
9988 * @author Vova Feldman (@svovaf)
9989 * @since 1.0.1
9990 * @since 1.2.2.5 If slug not set load slug by module ID.
9991 *
9992 * @return string Plugin slug.
9993 */
9994 function get_slug() {
9995 if ( ! isset( $this->_slug ) ) {
9996 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9997 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9998 }
9999
10000 return $this->_slug;
10001 }
10002
10003 /**
10004 * @author Leo Fajardo (@leorw)
10005 * @since 2.2.1
10006 *
10007 * @return string
10008 */
10009 function get_premium_slug() {
10010 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10011 $this->_plugin->premium_slug :
10012 "{$this->_slug}-premium";
10013 }
10014
10015 /**
10016 * Retrieve the desired folder name for the product.
10017 *
10018 * @author Vova Feldman (@svovaf)
10019 * @since 1.2.1.7
10020 *
10021 * @return string Plugin slug.
10022 */
10023 function get_target_folder_name() {
10024 return $this->can_use_premium_code() ?
10025 $this->_plugin->premium_slug :
10026 $this->_slug;
10027 }
10028
10029 /**
10030 * @author Vova Feldman (@svovaf)
10031 * @since 1.0.1
10032 *
10033 * @return number Plugin ID.
10034 */
10035 function get_id() {
10036 return $this->_plugin->id;
10037 }
10038
10039 /**
10040 * @author Leo Fajardo (@leorw)
10041 * @since 2.2.4
10042 *
10043 * @return number|null Bundle ID.
10044 */
10045 function get_bundle_id() {
10046 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10047 $this->_plugin->bundle_id :
10048 null;
10049 }
10050
10051 /**
10052 * @author Vova Feldman (@svovaf)
10053 * @since 2.3.1
10054 *
10055 * @return string|null Bundle public key.
10056 */
10057 function get_bundle_public_key() {
10058 return isset( $this->_plugin->bundle_public_key ) ?
10059 $this->_plugin->bundle_public_key :
10060 null;
10061 }
10062
10063 /**
10064 * Get whether the SDK has been initiated in the context of a Bundle.
10065 *
10066 * This will return true, if `bundle_id` is present in the SDK init parameters.
10067 *
10068 * ```php
10069 * $my_fs = fs_dynamic_init( array(
10070 * // ...
10071 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10072 * 'bundle_public_key' => 'pk_XXXX',
10073 * ) );
10074 * ```
10075 *
10076 * @author Swashata Ghosh (@swashata)
10077 * @since 2.5.0
10078 *
10079 * @return bool True if we are running in bundle context, false otherwise.
10080 */
10081 private function has_bundle_context() {
10082 return ! is_null( $this->get_bundle_id() );
10083 }
10084
10085 /**
10086 * @author Vova Feldman (@svovaf)
10087 * @since 1.2.1.5
10088 *
10089 * @return string Freemius SDK version
10090 */
10091 function get_sdk_version() {
10092 return $this->version;
10093 }
10094
10095 /**
10096 * @author Vova Feldman (@svovaf)
10097 * @since 1.2.1.5
10098 *
10099 * @return number Parent plugin ID (if parent exist).
10100 */
10101 function get_parent_id() {
10102 return $this->is_addon() ?
10103 $this->get_parent_instance()->get_id() :
10104 $this->_plugin->id;
10105 }
10106
10107 /**
10108 * @author Vova Feldman (@svovaf)
10109 * @since 2.3.1
10110 *
10111 * @return string
10112 */
10113 function get_usage_tracking_terms_url() {
10114 return $this->apply_filters(
10115 'usage_tracking_terms_url',
10116 "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/"
10117 );
10118 }
10119
10120 /**
10121 * @todo (For LiteSDK) We can refactor this and other related functions giving links to several landing pages on freemius.com to come from a separate class like `FS_Terms_Pages`. This would get a `FS_WP_Hook` (hypothetical) instance as a dependency and use it to hook into the `license_activation_terms_url` or related filters. The entry level instance from `ms_fs()` would hold a public read-only variable `my_fs()->terms_pages` which would be an instance of `FS_Terms_Pages` and would hold all the links to the terms pages.
10122 * @since 2.5.8
10123 *
10124 * @return string
10125 */
10126 function get_license_activation_terms_url() {
10127 return $this->apply_filters(
10128 'license_activation_terms_url',
10129 "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/"
10130 );
10131 }
10132
10133 /**
10134 * @author Vova Feldman (@svovaf)
10135 * @since 2.3.1
10136 *
10137 * @return string
10138 */
10139 function get_eula_url() {
10140 return $this->apply_filters(
10141 'eula_url',
10142 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10143 );
10144 }
10145
10146 /**
10147 * @author Vova Feldman (@svovaf)
10148 * @since 1.0.1
10149 *
10150 * @return string Plugin public key.
10151 */
10152 function get_public_key() {
10153 return $this->_plugin->public_key;
10154 }
10155
10156 /**
10157 * Will be available only on sandbox mode.
10158 *
10159 * @author Vova Feldman (@svovaf)
10160 * @since 1.0.4
10161 *
10162 * @return mixed Plugin secret key.
10163 */
10164 function get_secret_key() {
10165 return $this->_plugin->secret_key;
10166 }
10167
10168 /**
10169 * @author Vova Feldman (@svovaf)
10170 * @since 1.1.1
10171 *
10172 * @return bool
10173 */
10174 function has_secret_key() {
10175 return ! empty( $this->_plugin->secret_key );
10176 }
10177
10178 /**
10179 * @author Vova Feldman (@svovaf)
10180 * @since 1.0.9
10181 *
10182 * @param string|bool $premium_suffix
10183 *
10184 * @return string
10185 */
10186 function get_plugin_name( $premium_suffix = false ) {
10187 $this->_logger->entrance();
10188
10189 /**
10190 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10191 *
10192 * @author Vova Feldman
10193 */
10194 if ( ! isset( $this->_plugin_name ) ) {
10195 // Name is not yet set.
10196 $this->set_name( $premium_suffix );
10197 } else if (
10198 ! empty( $premium_suffix ) &&
10199 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10200 ) {
10201 // Name is already set, but there's a change in the premium suffix.
10202 $this->set_name( $premium_suffix );
10203 }
10204
10205 return $this->_plugin_name;
10206 }
10207
10208 /**
10209 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10210 *
10211 * @author Vova Feldman (@svovaf)
10212 * @since 2.2.1
10213 *
10214 * @param string $premium_suffix
10215 */
10216 private function set_name( $premium_suffix = '' ) {
10217 $plugin_data = $this->get_plugin_data();
10218
10219 // Get name.
10220 $this->_plugin_name = $plugin_data['Name'];
10221
10222 if ( is_string( $premium_suffix ) ) {
10223 $premium_suffix = trim( $premium_suffix );
10224
10225 if ( ! empty( $premium_suffix ) ) {
10226 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10227 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10228 $suffix_len = strlen( $suffix );
10229
10230 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10231 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10232 ) {
10233 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10234 }
10235 }
10236 }
10237
10238 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10239 }
10240
10241 /**
10242 * @author Vova Feldman (@svovaf)
10243 * @since 1.0.0
10244 *
10245 * @param bool $reparse_plugin_metadata
10246 *
10247 * @return string
10248 */
10249 function get_plugin_version( $reparse_plugin_metadata = false ) {
10250 $this->_logger->entrance();
10251
10252 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10253
10254 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10255
10256 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10257 }
10258
10259 /**
10260 * @author Vova Feldman (@svovaf)
10261 * @since 1.2.1.7
10262 *
10263 * @return string
10264 */
10265 function get_plugin_title() {
10266 $this->_logger->entrance();
10267
10268 $title = $this->_plugin->title;
10269
10270 return $this->apply_filters( 'plugin_title', $title );
10271 }
10272
10273 /**
10274 * @author Vova Feldman (@svovaf)
10275 * @since 1.2.2.7
10276 *
10277 * @param bool $lowercase
10278 *
10279 * @return string
10280 */
10281 function get_module_label( $lowercase = false ) {
10282 $label = $this->is_addon() ?
10283 $this->get_text_inline( 'Add-On', 'addon' ) :
10284 ( $this->is_plugin() ?
10285 $this->get_text_inline( 'Plugin', 'plugin' ) :
10286 $this->get_text_inline( 'Theme', 'theme' ) );
10287
10288 if ( $lowercase ) {
10289 $label = strtolower( $label );
10290 }
10291
10292 return $label;
10293 }
10294
10295 /**
10296 * @author Vova Feldman (@svovaf)
10297 * @since 1.0.4
10298 *
10299 * @return string
10300 */
10301 function get_plugin_basename() {
10302 if ( ! isset( $this->_plugin_basename ) ) {
10303 if ( $this->is_plugin() ) {
10304 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10305 } else {
10306 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10307 }
10308 }
10309
10310 return $this->_plugin_basename;
10311 }
10312
10313 function get_plugin_folder_name() {
10314 $this->_logger->entrance();
10315
10316 $plugin_folder = $this->_plugin_basename;
10317
10318 while ( '.' !== dirname( $plugin_folder ) ) {
10319 $plugin_folder = dirname( $plugin_folder );
10320 }
10321
10322 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10323
10324 return $plugin_folder;
10325 }
10326
10327 #endregion ------------------------------------------------------------------
10328
10329 /* Account
10330 ------------------------------------------------------------------------------------------------------------------*/
10331
10332 /**
10333 * Find plugin's slug by plugin's basename.
10334 *
10335 * @author Vova Feldman (@svovaf)
10336 * @since 1.0.9
10337 *
10338 * @param string $plugin_base_name
10339 *
10340 * @return false|string
10341 */
10342 private static function find_slug_by_basename( $plugin_base_name ) {
10343 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10344
10345 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10346 return false;
10347 }
10348
10349 return $file_slug_map[ $plugin_base_name ];
10350 }
10351
10352 /**
10353 * Store the map between the plugin's basename to the slug.
10354 *
10355 * @author Vova Feldman (@svovaf)
10356 * @since 1.0.9
10357 */
10358 private function store_file_slug_map() {
10359 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10360
10361 if ( ! array( $file_slug_map ) ) {
10362 $file_slug_map = array();
10363 }
10364
10365 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10366 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10367 ) {
10368 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10369 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10370 }
10371 }
10372
10373 /**
10374 * @return array[number]FS_User
10375 */
10376 static function get_all_users() {
10377 $users = self::maybe_get_entities_account_option( 'users', array() );
10378
10379 if ( ! is_array( $users ) ) {
10380 $users = array();
10381 }
10382
10383 return $users;
10384 }
10385
10386 /**
10387 * @param string $module_type
10388 * @param null|int $blog_id Since 2.0.0
10389 *
10390 * @return array[string]FS_Site
10391 */
10392 public static function get_all_sites(
10393 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10394 $blog_id = null,
10395 $is_backup = false
10396 ) {
10397 $sites = self::get_account_option(
10398 ( $is_backup ? 'prev_' : '' ) . 'sites',
10399 $module_type,
10400 $blog_id
10401 );
10402
10403 if ( ! is_array( $sites ) ) {
10404 $sites = array();
10405 }
10406
10407 return $sites;
10408 }
10409
10410 /**
10411 * @author Leo Fajardo (@leorw)
10412 *
10413 * @since 1.2.2
10414 *
10415 * @param string $option_name
10416 * @param string $module_type
10417 * @param null|int $network_level_or_blog_id Since 2.0.0
10418 *
10419 * @return mixed
10420 */
10421 public static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10422 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10423 $option_name = $module_type . '_' . $option_name;
10424 }
10425
10426 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10427 }
10428
10429 /**
10430 * @author Leo Fajardo (@leorw)
10431 *
10432 * @since 1.2.2
10433 *
10434 * @param string $option_name
10435 * @param mixed $option_value
10436 * @param bool $store
10437 * @param null|int $network_level_or_blog_id Since 2.0.0
10438 */
10439 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10440 self::set_account_option_by_module(
10441 $this->_module_type,
10442 $option_name,
10443 $option_value,
10444 $store,
10445 $network_level_or_blog_id
10446 );
10447 }
10448
10449 /**
10450 * @author Vova Feldman (@svovaf)
10451 *
10452 * @since 1.2.2.7
10453 *
10454 * @param string $module_type
10455 * @param string $option_name
10456 * @param mixed $option_value
10457 * @param bool $store
10458 * @param null|int $network_level_or_blog_id Since 2.0.0
10459 */
10460 private static function set_account_option_by_module(
10461 $module_type,
10462 $option_name,
10463 $option_value,
10464 $store,
10465 $network_level_or_blog_id = null
10466 ) {
10467 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10468 $option_name = $module_type . '_' . $option_name;
10469 }
10470
10471 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10472 }
10473
10474 /**
10475 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10476 *
10477 * @author Leo Fajardo (@leorw)
10478 * @since 2.3.1
10479 *
10480 * @param string $option_name
10481 * @param mixed $default
10482 * @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).
10483 *
10484 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10485 */
10486 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10487 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10488
10489 $class_name = '';
10490
10491 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10492 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10493 }
10494
10495 switch ( $option_name ) {
10496 case 'plugins':
10497 case 'themes':
10498 case 'addons':
10499 $class_name = FS_Plugin::get_class_name();
10500 break;
10501 case 'users':
10502 $class_name = FS_User::get_class_name();
10503 break;
10504 case 'sites':
10505 $class_name = FS_Site::get_class_name();
10506 break;
10507 case 'licenses':
10508 case 'all_licenses':
10509 $class_name = FS_Plugin_License::get_class_name();
10510 break;
10511 case 'plans':
10512 $class_name = FS_Plugin_Plan::get_class_name();
10513 break;
10514 case 'updates':
10515 $class_name = FS_Plugin_Tag::get_class_name();
10516 break;
10517 }
10518
10519 if ( empty( $class_name ) ) {
10520 return $option;
10521 }
10522
10523 return fs_get_entities( $option, $class_name );
10524 }
10525
10526 /**
10527 * @author Vova Feldman (@svovaf)
10528 * @since 1.0.6
10529 *
10530 * @param number|null $module_id
10531 *
10532 * @return FS_Plugin_License[]
10533 */
10534 private static function get_all_licenses( $module_id = null ) {
10535 $licenses = self::get_account_option( 'all_licenses' );
10536
10537 if ( ! is_array( $licenses ) ) {
10538 $licenses = array();
10539 }
10540
10541 if ( is_null( $module_id ) ) {
10542 return $licenses;
10543 }
10544
10545 $licenses = isset( $licenses[ $module_id ] ) ?
10546 $licenses[ $module_id ] :
10547 array();
10548
10549 return $licenses;
10550 }
10551
10552 /**
10553 * @author Leo Fajardo (@leorw)
10554 * @since 2.0.0
10555 *
10556 * @param number $module_id
10557 * @param number|null $user_id
10558 *
10559 * @return array
10560 */
10561 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
10562 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10563
10564 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10565 $all_modules_user_id_license_ids_map = array();
10566 }
10567
10568 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
10569 $all_modules_user_id_license_ids_map[ $module_id ] :
10570 array();
10571
10572 if ( FS_User::is_valid_id( $user_id ) ) {
10573 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
10574 $user_id_license_ids_map[ $user_id ] :
10575 array();
10576 }
10577
10578 return $user_id_license_ids_map;
10579 }
10580
10581 /**
10582 * @author Leo Fajardo (@leorw)
10583 * @since 2.0.0
10584 *
10585 * @param array $new_user_id_license_ids_map
10586 * @param number $module_id
10587 * @param number|null $user_id
10588 */
10589 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
10590 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
10591 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
10592 $all_modules_user_id_license_ids_map = array();
10593 }
10594
10595 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
10596 $all_modules_user_id_license_ids_map[ $module_id ] = array();
10597 }
10598
10599 if ( FS_User::is_valid_id( $user_id ) ) {
10600 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
10601 } else {
10602 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
10603 }
10604
10605 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
10606 }
10607
10608 /**
10609 * Get a collection of the user's linked license IDs.
10610 *
10611 * @author Vova Feldman (@svovaf)
10612 * @since 2.0.0
10613 *
10614 * @param number $user_id
10615 *
10616 * @return number[]
10617 */
10618 private function get_user_linked_license_ids( $user_id ) {
10619 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
10620 }
10621
10622 /**
10623 * Override the user's linked license IDs with a new IDs collection.
10624 *
10625 * @author Vova Feldman (@svovaf)
10626 * @since 2.0.0
10627 *
10628 * @param number $user_id
10629 * @param number[] $license_ids
10630 */
10631 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
10632 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
10633 }
10634
10635 /**
10636 * Link a specified license ID to a given user.
10637 *
10638 * @author Vova Feldman (@svovaf)
10639 * @since 2.0.0
10640 *
10641 * @param number $license_id
10642 * @param number $user_id
10643 */
10644 private function link_license_2_user( $license_id, $user_id ) {
10645 $license_ids = $this->get_user_linked_license_ids( $user_id );
10646
10647 if ( in_array( $license_id, $license_ids ) ) {
10648 // License already linked.
10649 return;
10650 }
10651
10652 $license_ids[] = $license_id;
10653
10654 $this->set_user_linked_license_ids( $user_id, $license_ids );
10655 }
10656
10657 /**
10658 * @param string|bool $module_type
10659 *
10660 * @return FS_Plugin_Plan[]
10661 */
10662 private static function get_all_plans( $module_type = false ) {
10663 $plans = self::get_account_option( 'plans', $module_type );
10664
10665 if ( ! is_array( $plans ) ) {
10666 $plans = array();
10667 }
10668
10669 return $plans;
10670 }
10671
10672 /**
10673 * @author Vova Feldman (@svovaf)
10674 * @since 1.0.4
10675 *
10676 * @return FS_Plugin_Tag[]
10677 */
10678 private static function get_all_updates() {
10679 $updates = self::maybe_get_entities_account_option( 'updates', array() );
10680
10681 if ( ! is_array( $updates ) ) {
10682 $updates = array();
10683 }
10684
10685 return $updates;
10686 }
10687
10688 /**
10689 * @author Vova Feldman (@svovaf)
10690 * @since 1.0.6
10691 *
10692 * @return array<number,FS_Plugin[]>|false
10693 */
10694 public static function get_all_addons() {
10695 $addons = self::maybe_get_entities_account_option( 'addons', array() );
10696
10697 if ( ! is_array( $addons ) ) {
10698 $addons = array();
10699 }
10700
10701 return $addons;
10702 }
10703
10704 /**
10705 * @author Vova Feldman (@svovaf)
10706 * @since 1.0.6
10707 *
10708 * @return number[]|false
10709 */
10710 public static function get_all_account_addons() {
10711 $addons = self::$_accounts->get_option( 'account_addons', array() );
10712
10713 if ( ! is_array( $addons ) ) {
10714 $addons = array();
10715 }
10716
10717 return $addons;
10718 }
10719
10720 /**
10721 * Check if user has connected his account (opted-in).
10722 *
10723 * Note:
10724 * If the user opted-in and opted-out on a later stage,
10725 * this will still return true. If you want to check if the
10726 * user is currently opted-in, use:
10727 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10728 *
10729 * @author Vova Feldman (@svovaf)
10730 * @since 1.0.1
10731 *
10732 * @param bool $ignore_anonymous_state Since 2.5.1
10733 *
10734 * @return bool
10735 */
10736 function is_registered( $ignore_anonymous_state = false ) {
10737 return (
10738 is_object( $this->_user ) &&
10739 (
10740 $this->is_premium() ||
10741 $ignore_anonymous_state ||
10742 ! $this->is_anonymous()
10743 )
10744 );
10745 }
10746
10747 /**
10748 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10749 *
10750 * @author Leo Fajardo (@leorw)
10751 * @since 1.2.1.5
10752 *
10753 * @return bool
10754 */
10755 function is_tracking_allowed( $blog_id = null, $install = null ) {
10756 if ( is_null( $install ) ) {
10757 $install = is_null( $blog_id ) ?
10758 $this->_site :
10759 $this->get_install_by_blog_id( $blog_id );
10760 }
10761
10762 return (
10763 is_object( $install ) &&
10764 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
10765 );
10766 }
10767
10768 /**
10769 * Returns TRUE if the user never opted-in or manually opted-out.
10770 *
10771 * @author Vova Feldman (@svovaf)
10772 * @since 1.2.1.5
10773 *
10774 * @param int|null $blog_id
10775 *
10776 * @return bool
10777 */
10778 function is_tracking_prohibited( $blog_id = null ) {
10779 return (
10780 ! $this->is_registered( true ) ||
10781 ! $this->is_tracking_allowed( $blog_id )
10782 );
10783 }
10784
10785 /**
10786 * @author Leo Fajardo (@leorw)
10787 * @since 2.4.0
10788 *
10789 * @return bool
10790 */
10791 function is_bundle_license_auto_activation_enabled() {
10792 return $this->is_addon() ?
10793 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
10794 $this->_is_bundle_license_auto_activation_enabled;
10795 }
10796
10797 /**
10798 * @author Vova Feldman (@svovaf)
10799 * @since 1.0.4
10800 *
10801 * @return FS_Plugin
10802 */
10803 function get_plugin() {
10804 return $this->_plugin;
10805 }
10806
10807 /**
10808 * @author Vova Feldman (@svovaf)
10809 * @since 1.0.3
10810 *
10811 * @return FS_User
10812 */
10813 function get_user() {
10814 return $this->_user;
10815 }
10816
10817 /**
10818 * @author Vova Feldman (@svovaf)
10819 * @since 1.0.3
10820 *
10821 * @return FS_Site
10822 */
10823 function get_site() {
10824 return $this->_site;
10825 }
10826
10827 /**
10828 * @author Daniele Alessandra (@danielealessandra)
10829 * @return FS_Storage
10830 * @since 2.6.2
10831 *
10832 */
10833 public function get_storage() {
10834 return $this->_storage;
10835 }
10836
10837 /**
10838 * @author Leo Fajardo (@leorw)
10839 * @since 2.5.0
10840 */
10841 function store_site( $site ) {
10842 $this->_site = $site;
10843 $this->_store_site( true );
10844 }
10845
10846 /**
10847 * Deletes the current install with an option to back it up in case restoration will be needed (e.g., if the automatic clone resolution attempt fails).
10848 *
10849 * @author Leo Fajardo (@leorw)
10850 * @since 2.5.0
10851 */
10852 function delete_current_install( $back_up ) {
10853 // Back up and delete the unique ID.
10854 if ( $back_up ) {
10855 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
10856 }
10857
10858 self::$_accounts->set_option( 'unique_id', null );
10859
10860 if ( $back_up ) {
10861 // Back up the install before deleting it so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails).
10862 $this->back_up_site();
10863 }
10864
10865 $this->_delete_site();
10866 $this->_site = null;
10867 }
10868
10869 /**
10870 * @author Leo Fajardo (@leorw)
10871 * @since 2.5.0
10872 */
10873 function restore_backup_site() {
10874 self::$_accounts->set_option(
10875 'unique_id',
10876 self::$_accounts->get_option( 'prev_unique_id' )
10877 );
10878
10879 $sites = self::get_all_sites( $this->_module_type, null, true );
10880 $this->store_site( clone $sites[ $this->_slug ] );
10881 }
10882
10883 /**
10884 * Get plugin add-ons.
10885 *
10886 * @author Vova Feldman (@svovaf)
10887 * @since 1.0.6
10888 *
10889 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10890 *
10891 * @param bool $flush
10892 *
10893 * @return FS_Plugin[]|false
10894 */
10895 function get_addons( $flush = false ) {
10896 $this->_logger->entrance();
10897
10898 if ( ! $this->_has_addons ) {
10899 return false;
10900 }
10901
10902 $addons = $this->sync_addons( $flush );
10903
10904 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10905 false :
10906 $addons;
10907 }
10908
10909 /**
10910 * @author Vova Feldman (@svovaf)
10911 * @since 1.0.6
10912 *
10913 * @return number[]|false
10914 */
10915 function get_account_addons() {
10916 $this->_logger->entrance();
10917
10918 $addons = self::get_all_account_addons();
10919
10920 if ( ! is_array( $addons ) ||
10921 ! isset( $addons[ $this->_plugin->id ] ) ||
10922 ! is_array( $addons[ $this->_plugin->id ] ) ||
10923 0 === count( $addons[ $this->_plugin->id ] )
10924 ) {
10925 return false;
10926 }
10927
10928 return $addons[ $this->_plugin->id ];
10929 }
10930
10931 /**
10932 * Check if user has any
10933 *
10934 * @author Vova Feldman (@svovaf)
10935 * @since 1.1.6
10936 *
10937 * @return bool
10938 */
10939 function has_account_addons() {
10940 $addons = $this->get_account_addons();
10941
10942 return is_array( $addons ) && ( 0 < count( $addons ) );
10943 }
10944
10945
10946 /**
10947 * Get add-on by ID (from local data).
10948 *
10949 * @author Vova Feldman (@svovaf)
10950 * @since 1.0.6
10951 *
10952 * @param number $id
10953 *
10954 * @return FS_Plugin|false
10955 */
10956 function get_addon( $id ) {
10957 $this->_logger->entrance();
10958
10959 $addons = $this->get_addons();
10960
10961 if ( is_array( $addons ) ) {
10962 foreach ( $addons as $addon ) {
10963 if ( $id == $addon->id ) {
10964 return $addon;
10965 }
10966 }
10967 }
10968
10969 return false;
10970 }
10971
10972 /**
10973 * Get add-on by slug (from local data).
10974 *
10975 * @author Vova Feldman (@svovaf)
10976 * @since 1.0.6
10977 *
10978 * @param string $slug
10979 *
10980 * @param bool $flush
10981 *
10982 * @return FS_Plugin|false
10983 */
10984 function get_addon_by_slug( $slug, $flush = false ) {
10985 $this->_logger->entrance();
10986
10987 $addons = $this->get_addons( $flush );
10988
10989 if ( is_array( $addons ) ) {
10990 foreach ( $addons as $addon ) {
10991 if ( $slug === $addon->slug ) {
10992 return $addon;
10993 }
10994 }
10995 }
10996
10997 return false;
10998 }
10999
11000 /**
11001 * @var array<number,object[]> {
11002 * @key number Add-on ID.
11003 * @val object[] The add-on's plans and prices object.
11004 * }
11005 */
11006 private $plans_and_pricing_by_addon_id;
11007
11008 /**
11009 * @author Leo Fajardo (@leorw)
11010 * @since 2.3.0
11011 *
11012 * @return array<number,object[]> {
11013 * @key number Add-on ID.
11014 * @val object[] The add-on's plans and prices object.
11015 * }
11016 */
11017 function _get_addons_plans_and_pricing_map_by_id() {
11018 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11019 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11020
11021 $plans_and_pricing_by_addon_id = array();
11022 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11023 foreach ( $result->addons as $addon ) {
11024 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11025 }
11026 }
11027
11028 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11029 }
11030
11031 return $this->plans_and_pricing_by_addon_id;
11032 }
11033
11034 /**
11035 * @author Leo Fajardo (@leorw)
11036 * @since 2.3.0
11037 *
11038 * @param number $addon_id
11039 * @param bool $is_installed
11040 *
11041 * @return array
11042 */
11043 function _get_addon_info( $addon_id, $is_installed ) {
11044 $addon = $this->get_addon( $addon_id );
11045
11046 if ( ! is_object( $addon ) ) {
11047 // Unexpected call.
11048 return array();
11049 }
11050
11051 $slug = $addon->slug;
11052
11053 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11054
11055 if ( ! fs_is_network_admin() ) {
11056 // Get blog-level activated installations.
11057 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11058 } else {
11059 $sites = null;
11060
11061 if ( $this->is_addon_activated( $addon_id ) &&
11062 $this->get_addon_instance( $addon_id )->is_network_active()
11063 ) {
11064 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11065 // Get network-level activated installations.
11066 $sites = self::maybe_get_entities_account_option(
11067 'sites',
11068 array(),
11069 $addon_storage->network_install_blog_id
11070 );
11071 }
11072 }
11073 }
11074
11075 $addon_info = array(
11076 'is_connected' => false,
11077 'slug' => $slug,
11078 'title' => $addon->title,
11079 'is_whitelabeled' => $addon_storage->is_whitelabeled
11080 );
11081
11082 if ( ! $is_installed ) {
11083 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11084
11085 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11086 $has_paid_plan = false;
11087 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11088
11089 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11090 foreach ( $plans as $plan ) {
11091 if ( isset( $plan->pricing ) &&
11092 is_array( $plan->pricing ) &&
11093 count( $plan->pricing ) > 0
11094 ) {
11095 $has_paid_plan = true;
11096 break;
11097 }
11098 }
11099 }
11100
11101 $addon_info['has_paid_plan'] = $has_paid_plan;
11102 }
11103 }
11104
11105 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11106 return $addon_info;
11107 }
11108
11109 $site = $sites[ $slug ];
11110
11111 $addon_info['is_connected'] = (
11112 ( $addon->parent_plugin_id == $this->get_id() ) &&
11113 is_object( $site ) &&
11114 FS_Site::is_valid_id( $site->id ) &&
11115 FS_User::is_valid_id( $site->user_id ) &&
11116 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11117 );
11118
11119 if ( $addon_info['is_connected'] && $is_installed ) {
11120 return $addon_info;
11121 }
11122
11123 $addon_info['site'] = $site;
11124
11125 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11126 if ( isset( $plugins_data[ $slug ] ) ) {
11127 $plugin_data = $plugins_data[ $slug ];
11128
11129 $addon_info['version'] = $plugin_data->version;
11130 }
11131
11132 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11133 if ( isset( $all_plans[ $slug ] ) ) {
11134 $plans = $all_plans[ $slug ];
11135
11136 foreach ( $plans as $plan ) {
11137 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11138 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11139 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11140 break;
11141 }
11142 }
11143 }
11144
11145 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11146 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11147 foreach ( $licenses[ $addon_id ] as $license ) {
11148 if ( $license->id == $site->license_id ) {
11149 $addon_info['license'] = $license;
11150 break;
11151 }
11152 }
11153 }
11154
11155 if ( isset( $addon_info['license'] ) ) {
11156 if ( isset( $addon_storage->subscriptions ) &&
11157 ! empty( $addon_storage->subscriptions )
11158 ) {
11159 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11160
11161 foreach ( $addon_subscriptions as $subscription ) {
11162 if ( $subscription->license_id == $site->license_id ) {
11163 $addon_info['subscription'] = $subscription;
11164 break;
11165 }
11166 }
11167 }
11168 }
11169
11170 return $addon_info;
11171 }
11172
11173 /**
11174 * @author Vova Feldman (@svovaf)
11175 * @since 2.0.0
11176 *
11177 * @param number $user_id
11178 *
11179 * @return FS_User
11180 */
11181 static function _get_user_by_id( $user_id ) {
11182 self::$_static_logger->entrance( "user_id = {$user_id}" );
11183
11184 $users = self::get_all_users();
11185
11186 if ( is_array( $users ) ) {
11187 if ( isset( $users[ $user_id ] ) &&
11188 $users[ $user_id ] instanceof FS_User &&
11189 $user_id == $users[ $user_id ]->id
11190 ) {
11191 return $users[ $user_id ];
11192 }
11193
11194 // If user wasn't found by the key, iterate over all the users collection.
11195 foreach ( $users as $user ) {
11196 /**
11197 * @var FS_User $user
11198 */
11199 if ( $user_id == $user->id ) {
11200 return $user;
11201 }
11202 }
11203 }
11204
11205 return null;
11206 }
11207
11208 /**
11209 * Checks if a Freemius user_id is associated with a super-admin.
11210 *
11211 * @author Vova Feldman (@svovaf)
11212 * @since 2.0.0
11213 *
11214 * @param number $user_id
11215 *
11216 * @return bool
11217 */
11218 private static function is_super_admin( $user_id ) {
11219 $is_super_admin = false;
11220
11221 $user = self::_get_user_by_id( $user_id );
11222
11223 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11224 self::require_pluggable_essentials();
11225
11226 $wp_user = get_user_by( 'email', $user->email );
11227
11228 if ( $wp_user instanceof WP_User ) {
11229 $super_admins = get_super_admins();
11230 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11231 }
11232 }
11233
11234 return $is_super_admin;
11235 }
11236
11237 #----------------------------------------------------------------------------------
11238 #region Plans & Licensing
11239 #----------------------------------------------------------------------------------
11240
11241 /**
11242 * Check if running premium plugin code.
11243 *
11244 * @author Vova Feldman (@svovaf)
11245 * @since 1.0.5
11246 *
11247 * @return bool
11248 */
11249 function is_premium() {
11250 /**
11251 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11252 * `register_constructor_hooks` method.
11253 *
11254 * @author Leo Fajardo (@leorw)
11255 * @since 2.2.3
11256 */
11257 return is_object( $this->_plugin ) ?
11258 $this->_plugin->is_premium :
11259 false;
11260 }
11261
11262 /**
11263 * Get site's plan ID.
11264 *
11265 * @author Vova Feldman (@svovaf)
11266 * @since 1.0.2
11267 *
11268 * @return number
11269 */
11270 function get_plan_id() {
11271 return $this->_site->plan_id;
11272 }
11273
11274 /**
11275 * Get site's plan title.
11276 *
11277 * @author Vova Feldman (@svovaf)
11278 * @since 1.0.2
11279 *
11280 * @return string
11281 */
11282 function get_plan_title() {
11283 $plan = $this->get_plan();
11284
11285 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11286 }
11287
11288 /**
11289 * Get site's plan name.
11290 *
11291 * @author Vova Feldman (@svovaf)
11292 * @since 2.0.0
11293 *
11294 * @return string
11295 */
11296 function get_plan_name() {
11297 $plan = $this->get_plan();
11298
11299 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11300 }
11301
11302 /**
11303 * @author Vova Feldman (@svovaf)
11304 * @since 1.0.9
11305 *
11306 * @return FS_Plugin_Plan|false
11307 */
11308 function get_plan() {
11309 if ( ! is_object( $this->_site ) ) {
11310 return false;
11311 }
11312
11313 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11314 $this->_get_plan_by_id( $this->_site->plan_id ) :
11315 false;
11316 }
11317
11318 /**
11319 * @author Vova Feldman (@svovaf)
11320 * @since 1.0.3
11321 *
11322 * @return bool
11323 */
11324 function is_trial() {
11325 $this->_logger->entrance();
11326
11327 if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) {
11328 return false;
11329 }
11330
11331 return $this->_site->is_trial();
11332 }
11333
11334 /**
11335 * Check if currently in a trial with payment method (credit card or paypal).
11336 *
11337 * @author Vova Feldman (@svovaf)
11338 * @since 1.1.7
11339 *
11340 * @return bool
11341 */
11342 function is_paid_trial() {
11343 $this->_logger->entrance();
11344
11345 if ( ! $this->is_trial() ) {
11346 return false;
11347 }
11348
11349 if ( ! $this->has_active_valid_license() ) {
11350 return false;
11351 }
11352
11353 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11354 return false;
11355 }
11356
11357 /**
11358 * @var FS_Subscription $subscription
11359 */
11360 $subscription = $this->_get_subscription( $this->_license->id );
11361
11362 return ( is_object( $subscription ) && $subscription->is_active() );
11363 }
11364
11365 /**
11366 * Check if trial already utilized.
11367 *
11368 * @since 1.0.9
11369 *
11370 * @return bool
11371 */
11372 function is_trial_utilized() {
11373 $this->_logger->entrance();
11374
11375 if ( ! $this->is_registered() ) {
11376 return false;
11377 }
11378
11379 return $this->_site->is_trial_utilized();
11380 }
11381
11382 /**
11383 * Get trial plan information (if in trial).
11384 *
11385 * @author Vova Feldman (@svovaf)
11386 * @since 1.0.9
11387 *
11388 * @return bool|FS_Plugin_Plan
11389 */
11390 function get_trial_plan() {
11391 $this->_logger->entrance();
11392
11393 if ( ! $this->is_trial() ) {
11394 return false;
11395 }
11396
11397 // Try to load plan from local cache.
11398 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11399
11400 if ( ! is_object( $trial_plan ) ) {
11401 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11402
11403 /**
11404 * If managed to fetch the plan, add it to the plans collection.
11405 */
11406 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11407 if ( ! is_array( $this->_plans ) ) {
11408 $this->_plans = array();
11409 }
11410
11411 $this->_plans[] = $trial_plan;
11412 $this->_store_plans();
11413 }
11414 }
11415
11416 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11417 return $trial_plan;
11418 }
11419
11420 /**
11421 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11422 */
11423 $trial_plan = new FS_Plugin_Plan();
11424 $trial_plan->id = $this->_site->trial_plan_id;
11425 $trial_plan->name = 'pro';
11426 $trial_plan->title = 'Pro';
11427
11428 return $trial_plan;
11429 }
11430
11431 /**
11432 * Check if the user has an activate, non-expired license on current plugin's install.
11433 *
11434 * @since 1.0.9
11435 *
11436 * @return bool
11437 */
11438 function is_paying() {
11439 $this->_logger->entrance();
11440
11441 if ( ! $this->is_registered( true ) ) {
11442 return false;
11443 }
11444
11445 if ( ! $this->has_paid_plan() ) {
11446 return false;
11447 }
11448
11449 return (
11450 ! $this->is_trial() &&
11451 'free' !== $this->get_plan_name() &&
11452 $this->has_active_valid_license()
11453 );
11454 }
11455
11456 /**
11457 * @author Vova Feldman (@svovaf)
11458 * @since 1.0.4
11459 *
11460 * @return bool
11461 */
11462 function is_free_plan() {
11463 if ( ! $this->is_registered() ) {
11464 return true;
11465 }
11466
11467 if ( ! $this->has_paid_plan() ) {
11468 return true;
11469 }
11470
11471 return (
11472 'free' === $this->get_plan_name() ||
11473 ! $this->has_features_enabled_license()
11474 );
11475 }
11476
11477 /**
11478 * @author Vova Feldman (@svovaf)
11479 * @since 1.0.5
11480 *
11481 * @return bool
11482 */
11483 function _has_premium_license() {
11484 $this->_logger->entrance();
11485
11486 $premium_license = $this->_get_available_premium_license();
11487
11488 return ( false !== $premium_license );
11489 }
11490
11491 /**
11492 * Check if user has any licenses associated with the plugin (including expired or blocking).
11493 *
11494 * @author Vova Feldman (@svovaf)
11495 * @since 1.1.7.3
11496 *
11497 * @param bool $including_foreign
11498 *
11499 * @return bool
11500 */
11501 function has_any_license( $including_foreign = true ) {
11502 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11503 return false;
11504 }
11505
11506 if ( $including_foreign ) {
11507 return true;
11508 }
11509
11510 foreach ( $this->_licenses as $license ) {
11511 if ( $this->_user->id == $license->user_id ) {
11512 return true;
11513 }
11514 }
11515
11516 return false;
11517 }
11518
11519 /**
11520 * @author Vova Feldman (@svovaf)
11521 * @since 1.0.5
11522 *
11523 * @param bool|null $is_localhost
11524 *
11525 * @return FS_Plugin_License|false
11526 */
11527 function _get_available_premium_license( $is_localhost = null ) {
11528 $this->_logger->entrance();
11529
11530 $licenses = $this->get_available_premium_licenses( $is_localhost );
11531 if ( ! empty( $licenses ) ) {
11532 return $licenses[0];
11533 }
11534
11535 return false;
11536 }
11537
11538 /**
11539 * @author Vova Feldman (@svovaf)
11540 * @since 1.0.5
11541 *
11542 * @param bool|null $is_localhost
11543 *
11544 * @return FS_Plugin_License[]
11545 */
11546 function get_available_premium_licenses( $is_localhost = null ) {
11547 $this->_logger->entrance();
11548
11549 $licenses = array();
11550 if ( ! $this->has_paid_plan() ) {
11551 return $licenses;
11552 }
11553
11554 if ( is_array( $this->_licenses ) ) {
11555 foreach ( $this->_licenses as $license ) {
11556 if ( ! $license->can_activate( $is_localhost ) ) {
11557 continue;
11558 }
11559
11560 $licenses[] = $license;
11561 }
11562 }
11563
11564 return $licenses;
11565 }
11566
11567 /**
11568 * Sync local plugin plans with remote server.
11569 *
11570 * 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).
11571 *
11572 * @author Vova Feldman (@svovaf)
11573 * @since 1.0.5
11574 *
11575 * @return FS_Plugin_Plan[]|object
11576 */
11577 function _sync_plans() {
11578 $plans = $this->_fetch_plugin_plans();
11579
11580 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
11581 $plans_map = array();
11582 foreach ( $plans as $plan ) {
11583 $plans_map[ $plan->id ] = true;
11584 }
11585
11586 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
11587
11588 foreach ( $plans_ids_to_keep as $plan_id ) {
11589 if ( isset( $plans_map[ $plan_id ] ) ) {
11590 continue;
11591 }
11592
11593 $missing_plan = self::_get_plan_by_id( $plan_id, false );
11594
11595 if ( is_object( $missing_plan ) ) {
11596 $plans[] = $missing_plan;
11597 }
11598 }
11599
11600 $this->_plans = $plans;
11601 $this->_store_plans();
11602 }
11603
11604 $this->do_action( 'after_plans_sync', $plans );
11605
11606 return $this->_plans;
11607 }
11608
11609 /**
11610 * Check if specified plan exists locally. If not, fetch it and store it.
11611 *
11612 * @author Vova Feldman (@svovaf)
11613 * @since 2.0.0
11614 *
11615 * @param number $plan_id
11616 *
11617 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11618 */
11619 private function sync_plan_if_not_exist( $plan_id ) {
11620 $plan = self::_get_plan_by_id( $plan_id );
11621
11622 if ( is_object( $plan ) ) {
11623 // Plan already exists.
11624 return $plan;
11625 }
11626
11627 $plan = $this->fetch_plan_by_id( $plan_id );
11628
11629 if ( $plan instanceof FS_Plugin_Plan ) {
11630 $this->_plans[] = $plan;
11631 $this->_store_plans();
11632
11633 return $plan;
11634 }
11635
11636 return $plan;
11637 }
11638
11639 /**
11640 * Check if specified license exists locally. If not, fetch it and store it.
11641 *
11642 * @author Vova Feldman (@svovaf)
11643 * @since 2.0.0
11644 *
11645 * @param number $license_id
11646 * @param string $license_key
11647 *
11648 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
11649 */
11650 private function sync_license_if_not_exist( $license_id, $license_key ) {
11651 $license = $this->_get_license_by_id( $license_id );
11652
11653 if ( is_object( $license ) ) {
11654 // License already exists.
11655 return $license;
11656 }
11657
11658 $license = $this->fetch_license_by_key( $license_id, $license_key );
11659
11660 if ( $license instanceof FS_Plugin_License ) {
11661 $this->_licenses[] = $license;
11662
11663 $this->set_license( $license );
11664
11665 $this->_store_licenses();
11666
11667 return $license;
11668 }
11669
11670 return $license;
11671 }
11672
11673 /**
11674 * Get a collection of unique plan IDs that are associated with any installs in the network.
11675 *
11676 * @author Leo Fajardo (@leorw)
11677 * @since 2.0.0
11678 *
11679 * @return number[]
11680 */
11681 private function get_plans_ids_associated_with_installs() {
11682 if ( ! is_multisite() ) {
11683 if ( ! is_object( $this->_site ) ||
11684 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
11685 ) {
11686 return array();
11687 }
11688
11689 return array( $this->_site->plan_id );
11690 }
11691
11692 $plan_ids = array();
11693 $sites = self::get_sites();
11694 foreach ( $sites as $site ) {
11695 $blog_id = self::get_site_blog_id( $site );
11696 $install = $this->get_install_by_blog_id( $blog_id );
11697
11698 if ( ! is_object( $install ) ||
11699 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
11700 ) {
11701 continue;
11702 }
11703
11704 $plan_ids[ $install->plan_id ] = true;
11705 }
11706
11707 return array_keys( $plan_ids );
11708 }
11709
11710 /**
11711 * Get a collection of unique license IDs that are associated with any installs in the network.
11712 *
11713 * @author Leo Fajardo (@leorw)
11714 * @since 2.0.0
11715 *
11716 * @return number[]
11717 */
11718 private function get_license_ids_associated_with_installs() {
11719 if ( ! $this->_is_network_active ) {
11720 if ( ! is_object( $this->_site ) ||
11721 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
11722 ) {
11723 return array();
11724 }
11725
11726 return array( $this->_site->license_id );
11727 }
11728
11729 $license_ids = array();
11730 $sites = self::get_sites();
11731 foreach ( $sites as $site ) {
11732 $blog_id = self::get_site_blog_id( $site );
11733 $install = $this->get_install_by_blog_id( $blog_id );
11734
11735 if ( ! is_object( $install ) ||
11736 ! FS_Plugin_License::is_valid_id( $install->license_id )
11737 ) {
11738 continue;
11739 }
11740
11741 $license_ids[ $install->license_id ] = true;
11742 }
11743
11744 return array_keys( $license_ids );
11745 }
11746
11747 /**
11748 * @author Vova Feldman (@svovaf)
11749 * @since 1.0.5
11750 *
11751 * @param number $id
11752 *
11753 * @return FS_Plugin_Plan|false
11754 */
11755 function _get_plan_by_id( $id, $allow_sync = true ) {
11756 $this->_logger->entrance();
11757
11758 if ( $allow_sync && ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) ) {
11759 $this->_sync_plans();
11760 }
11761
11762 foreach ( $this->_plans as $plan ) {
11763 if ( $id == $plan->id ) {
11764 return $plan;
11765 }
11766 }
11767
11768 return false;
11769 }
11770
11771 /**
11772 * @author Vova Feldman (@svovaf)
11773 * @since 1.1.8.1
11774 *
11775 * @param string $name
11776 *
11777 * @return FS_Plugin_Plan|false
11778 */
11779 private function get_plan_by_name( $name ) {
11780 $this->_logger->entrance();
11781
11782 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11783 $this->_sync_plans();
11784 }
11785
11786 foreach ( $this->_plans as $plan ) {
11787 if ( $name == $plan->name ) {
11788 return $plan;
11789 }
11790 }
11791
11792 return false;
11793 }
11794
11795 /**
11796 * Sync local licenses with remote server.
11797 *
11798 * @author Vova Feldman (@svovaf)
11799 * @since 1.0.6
11800 *
11801 * @param number|bool $site_license_id
11802 * @param number|null $blog_id
11803 *
11804 * @return FS_Plugin_License[]|object
11805 */
11806 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11807 $this->_logger->entrance();
11808
11809 $is_network_admin = fs_is_network_admin();
11810
11811 if ( $is_network_admin && is_null( $blog_id ) ) {
11812 $all_licenses = self::get_all_licenses( $this->_module_id );
11813 } else {
11814 $all_licenses = $this->get_user_licenses( $this->_user->id );
11815 }
11816
11817 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11818
11819 $all_licenses_map = array();
11820 foreach ( $all_licenses as $license ) {
11821 $all_licenses_map[ $license->id ] = true;
11822 }
11823
11824 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11825
11826 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11827 $licenses_map = array();
11828 foreach ( $licenses as $license ) {
11829 $licenses_map[ $license->id ] = true;
11830 }
11831
11832 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11833 // foreach ( $license_ids_to_keep as $license_id ) {
11834 // if ( isset( $licenses_map[ $license_id ] ) ) {
11835 // continue;
11836 // }
11837 //
11838 // $missing_license = self::_get_license_by_id( $license_id, false );
11839 // if ( is_object( $missing_license ) ) {
11840 // $licenses[] = $missing_license;
11841 // $licenses_map[ $missing_license->id ] = true;
11842 // }
11843 // }
11844
11845 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11846
11847 foreach ( $user_license_ids as $key => $license_id ) {
11848 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11849 // Remove access to licenses that no longer exist.
11850 unset( $user_license_ids[ $key ] );
11851 }
11852 }
11853
11854 if ( ! empty( $user_license_ids ) ) {
11855 foreach ( $licenses_map as $license_id => $value ) {
11856 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11857 // Associate new licenses with the user who triggered the license syncing.
11858 $user_license_ids[] = $license_id;
11859 }
11860 }
11861
11862 $user_license_ids = array_unique( $user_license_ids );
11863 } else {
11864 $user_license_ids = array_keys( $licenses_map );
11865 }
11866
11867 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11868 $user_licenses = array();
11869 foreach ( $licenses as $license ) {
11870 if ( ! in_array( $license->id, $user_license_ids ) ) {
11871 continue;
11872 }
11873
11874 $user_licenses[] = $license;
11875 }
11876
11877 $this->_licenses = $user_licenses;
11878 } else {
11879 $this->_licenses = $licenses;
11880 }
11881
11882 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11883
11884 $this->_store_licenses( true, $this->_module_id, $licenses );
11885 }
11886
11887 // Update current license.
11888 if ( is_object( $this->_license ) ) {
11889 $license = $this->_get_license_by_id( $this->_license->id );
11890
11891 if ( is_object( $license ) ) {
11892 /**
11893 * `$license` can be `false` in case a user change action has just been completed and this method
11894 * has synced the `$this->_licenses` collection for the new user. In this case, the
11895 * `$this->_licenses` collection may have only the newly activated license that is associated with
11896 * the new user. `set_license` will eventually be called in the same request by the logic that
11897 * follows outside this method which will detect that the install's license has been updated, and
11898 * then `_update_site_license` will be called which in turn will call `set_license`.
11899 *
11900 * @author Leo Fajardo (@leorw)
11901 * @since 2.3.2
11902 */
11903 $this->set_license( $license );
11904 }
11905 }
11906
11907 return $this->_licenses;
11908 }
11909
11910 /**
11911 * @author Vova Feldman (@svovaf)
11912 * @since 1.0.5
11913 *
11914 * @param number $id
11915 * @param bool $sync_licenses
11916 *
11917 * @return FS_Plugin_License|false
11918 */
11919 function _get_license_by_id( $id, $sync_licenses = true ) {
11920 $this->_logger->entrance();
11921
11922 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11923 return false;
11924 }
11925
11926 /**
11927 * When running from the network level admin and opted-in from the network,
11928 * check if the license exists in the network user licenses collection.
11929 *
11930 * @author Vova Feldman (@svovaf)
11931 * @since 2.0.0
11932 */
11933 if ( fs_is_network_admin() &&
11934 $this->is_network_registered() &&
11935 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11936 ) {
11937 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11938
11939 foreach ( $licenses as $license ) {
11940 if ( $id == $license->id ) {
11941 return $license;
11942 }
11943 }
11944 }
11945
11946 if ( ! $this->has_any_license() && $sync_licenses ) {
11947 $this->_sync_licenses( $id );
11948 }
11949
11950 if ( is_array( $this->_licenses ) ) {
11951 foreach ( $this->_licenses as $license ) {
11952 if ( $id == $license->id ) {
11953 return $license;
11954 }
11955 }
11956 }
11957
11958 return false;
11959 }
11960
11961 /**
11962 * 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.
11963 *
11964 * @author Vova Feldman (@svovaf)
11965 * @since 2.0.0
11966 *
11967 * @param number $id
11968 *
11969 * @return FS_Plugin_License
11970 */
11971 private function get_license_by_id( $id ) {
11972 $licenses = self::get_all_licenses( $this->_module_id );
11973
11974 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11975 foreach ( $licenses as $license ) {
11976 if ( $id == $license->id ) {
11977 return $license;
11978 }
11979 }
11980 }
11981
11982 return null;
11983 }
11984
11985 /**
11986 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11987 *
11988 * @author Vova Feldman (@svovaf)
11989 * @since 2.0.0
11990 *
11991 * @return \FS_Plugin_License|mixed
11992 */
11993 private function sync_site_license() {
11994 $api = $this->get_api_user_scope();
11995
11996 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11997
11998 if ( ! $this->is_api_result_entity( $result ) ) {
11999 return $result;
12000 }
12001
12002 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12003 $this->_store_licenses();
12004
12005 return $license;
12006 }
12007
12008 /**
12009 * Get all user's available licenses for the current module.
12010 *
12011 * @author Vova Feldman (@svovaf)
12012 * @since 2.0.0
12013 *
12014 * @param number $user_id
12015 *
12016 * @return FS_Plugin_License[]
12017 */
12018 private function get_user_licenses( $user_id ) {
12019 $all_licenses = self::get_all_licenses( $this->_module_id );
12020 if ( empty( $all_licenses ) ) {
12021 return array();
12022 }
12023
12024 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12025 if ( empty( $user_license_ids ) ) {
12026 return array();
12027 }
12028
12029 $licenses = array();
12030 foreach ( $all_licenses as $license ) {
12031 if ( in_array( $license->id, $user_license_ids ) ) {
12032 $licenses[] = $license;
12033 }
12034 }
12035
12036 return $licenses;
12037 }
12038
12039 /**
12040 * Checks if the context license is network activated except on the given blog ID.
12041 *
12042 * @author Vova Feldman (@svovaf)
12043 * @since 2.0.0
12044 *
12045 * @param int $except_blog_id
12046 *
12047 * @return bool
12048 */
12049 private function is_license_network_active( $except_blog_id = 0 ) {
12050 $this->_logger->entrance();
12051
12052 if ( ! is_object( $this->_license ) ) {
12053 return false;
12054 }
12055
12056 $sites = self::get_sites();
12057
12058 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12059 // There are more sites than the number of activations, so license cannot be network activated.
12060 return false;
12061 }
12062
12063 foreach ( $sites as $site ) {
12064 $blog_id = self::get_site_blog_id( $site );
12065
12066 if ( $except_blog_id == $blog_id ) {
12067 // Skip excluded blog.
12068 continue;
12069 }
12070
12071 $install = $this->get_install_by_blog_id( $blog_id );
12072
12073 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12074 return false;
12075 }
12076 }
12077
12078 return true;
12079 }
12080
12081 /**
12082 * 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.
12083 *
12084 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12085 *
12086 * @author Vova Feldman (@svovaf)
12087 * @since 2.0.0
12088 *
12089 * @param \FS_User $user
12090 * @param \FS_Plugin_License $license
12091 *
12092 * @return bool
12093 */
12094 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12095 $this->_logger->entrance();
12096
12097 $result = $this->can_activate_license_on_network( $license );
12098
12099 if ( false === $result ) {
12100 return false;
12101 }
12102
12103 $installs_without_license = $result['installs'];
12104 if ( ! empty( $installs_without_license ) ) {
12105 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12106 }
12107
12108 $disconnected_site_ids = $result['sites'];
12109 if ( ! empty( $disconnected_site_ids ) ) {
12110 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12111 }
12112
12113 $this->link_license_2_user( $license->id, $user->id );
12114
12115 // Sync license after activations.
12116 $license->activated += $result['production_count'];
12117 $license->activated_local += $result['localhost_count'];
12118
12119 // $this->_store_licenses()
12120
12121 return true;
12122 }
12123
12124 /**
12125 * Checks if the given license can be activated on the whole network.
12126 *
12127 * @author Vova Feldman (@svovaf)
12128 * @since 2.0.0
12129 *
12130 * @param \FS_Plugin_License $license
12131 *
12132 * @return false|array {
12133 * @type array[int]FS_Site $installs Blog ID to install map.
12134 * @type int[] $sites Non-connected blog IDs.
12135 * @type int $production_count Production sites count.
12136 * @type int $localhost_count Production sites count.
12137 * }
12138 */
12139 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12140 $sites = self::get_sites();
12141
12142 $production_count = 0;
12143 $localhost_count = 0;
12144
12145 $installs_without_license = array();
12146 $disconnected_site_ids = array();
12147
12148 foreach ( $sites as $site ) {
12149 $blog_id = self::get_site_blog_id( $site );
12150 $install = $this->get_install_by_blog_id( $blog_id );
12151
12152 if ( is_object( $install ) ) {
12153 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12154 // License already activated on the install.
12155 continue;
12156 }
12157
12158 $url = $install->url;
12159
12160 $installs_without_license[ $blog_id ] = $install;
12161 } else {
12162 $url = is_object( $site ) ?
12163 $site->siteurl :
12164 self::get_unfiltered_site_url( $blog_id );
12165
12166 $disconnected_site_ids[] = $blog_id;
12167 }
12168
12169 if ( FS_Site::is_localhost_by_address( $url ) ) {
12170 $localhost_count ++;
12171 } else {
12172 $production_count ++;
12173 }
12174 }
12175
12176 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12177 return false;
12178 }
12179
12180 return array(
12181 'installs' => $installs_without_license,
12182 'sites' => $disconnected_site_ids,
12183 'production_count' => $production_count,
12184 'localhost_count' => $localhost_count,
12185 );
12186 }
12187
12188 /**
12189 * Activate a given license on a collection of installs.
12190 *
12191 * @author Vova Feldman (@svovaf)
12192 * @since 2.0.0
12193 *
12194 * @param \FS_User $user
12195 * @param string $license_key
12196 * @param array $blog_2_install_map {
12197 * @key int Blog ID.
12198 * @value FS_Site Blog's associated install.
12199 * }
12200 *
12201 * @return mixed|true
12202 */
12203 private function activate_license_on_many_installs(
12204 FS_User $user,
12205 $license_key,
12206 array $blog_2_install_map
12207 ) {
12208 $params = array(
12209 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12210 );
12211
12212 $install_2_blog_map = array();
12213 foreach ( $blog_2_install_map as $blog_id => $install ) {
12214 $params[] = array( 'id' => $install->id, 'url' => $install->url );
12215
12216 $install_2_blog_map[ $install->id ] = $blog_id;
12217 }
12218
12219 $result = $this->get_api_user_scope_by_user( $user )->call(
12220 "plugins/{$this->_plugin->id}/installs.json",
12221 'PUT',
12222 $params
12223 );
12224
12225 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12226 return $result;
12227 }
12228
12229 foreach ( $result->installs as $r_install ) {
12230 $install = new FS_Site( $r_install );
12231 $install->is_disconnected = false;
12232
12233 // Update install.
12234 $this->_store_site(
12235 true,
12236 $install_2_blog_map[ $r_install->id ],
12237 $install
12238 );
12239 }
12240
12241 return true;
12242 }
12243
12244 /**
12245 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12246 *
12247 * @author Vova Feldman (@svovaf)
12248 * @since 2.3.1
12249 *
12250 * @param \FS_User $user
12251 * @param string $license_key
12252 *
12253 * @return true|mixed True if successful, otherwise, the API result.
12254 */
12255 private function activate_license_on_site( FS_User $user, $license_key ) {
12256 return $this->activate_license_on_many_sites( $user, $license_key );
12257 }
12258
12259 /**
12260 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12261 *
12262 * @author Vova Feldman (@svovaf)
12263 * @since 2.0.0
12264 *
12265 * @param \FS_User $user
12266 * @param string $license_key
12267 * @param int[] $site_ids
12268 *
12269 * @return true|mixed True if successful, otherwise, the API result.
12270 */
12271 private function activate_license_on_many_sites(
12272 FS_User $user,
12273 $license_key,
12274 array $site_ids = array()
12275 ) {
12276 $sites = array();
12277 foreach ( $site_ids as $site_id ) {
12278 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12279 }
12280
12281 // Install the plugin.
12282 $result = $this->create_installs_with_user(
12283 $user,
12284 $license_key,
12285 false,
12286 $sites,
12287 false,
12288 true
12289 );
12290
12291 if ( ! $this->is_api_result_entity( $result ) &&
12292 ! $this->is_api_result_object( $result, 'installs' )
12293 ) {
12294 return $result;
12295 }
12296
12297 $installs = array();
12298
12299 if ( $this->is_api_result_entity( $result ) ) {
12300 $install = new FS_Site( $result );
12301
12302 $this->_user = $user;
12303
12304 $this->_store_site( true, null, $install );
12305
12306 $this->_site = $install;
12307
12308 $this->reset_anonymous_mode();
12309 } else {
12310 foreach ( $result->installs as $install ) {
12311 $installs[] = new FS_Site( $install );
12312 }
12313
12314 // Map site addresses to their blog IDs.
12315 $address_to_blog_map = $this->get_address_to_blog_map();
12316
12317 $first_blog_id = null;
12318
12319 foreach ( $installs as $install ) {
12320 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12321 $blog_id = $address_to_blog_map[ $address ];
12322
12323 $this->_store_site( true, $blog_id, $install );
12324
12325 $this->reset_anonymous_mode( $blog_id );
12326
12327 if ( is_null( $first_blog_id ) ) {
12328 $first_blog_id = $blog_id;
12329 }
12330 }
12331
12332 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12333 $this->_storage->network_install_blog_id = $first_blog_id;
12334 }
12335 }
12336
12337 return true;
12338 }
12339
12340 /**
12341 * Sync site's license with user licenses.
12342 *
12343 * @author Vova Feldman (@svovaf)
12344 * @since 1.0.6
12345 *
12346 * @param FS_Plugin_License|null $new_license
12347 *
12348 * @return FS_Plugin_License|null
12349 */
12350 function _update_site_license( $new_license ) {
12351 $this->_logger->entrance();
12352
12353 /**
12354 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12355 * accordingly so that it will also handle the case when an ownership change is done via license
12356 * activation.
12357 *
12358 * @author Leo Fajardo (@leorw)
12359 * @since 2.3.2
12360 */
12361 $this->set_license( $new_license );
12362
12363 if ( ! is_object( $new_license ) ) {
12364 $this->_site->license_id = null;
12365 $this->_sync_site_subscription( null );
12366
12367 return $this->_license;
12368 }
12369
12370 $this->_site->license_id = $this->_license->id;
12371
12372 if ( ! is_array( $this->_licenses ) ) {
12373 $this->_licenses = array();
12374 }
12375
12376 $is_license_found = false;
12377 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12378 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12379 $this->_licenses[ $i ] = $new_license;
12380
12381 $is_license_found = true;
12382 break;
12383 }
12384 }
12385
12386 // If new license just append.
12387 if ( ! $is_license_found ) {
12388 $this->_licenses[] = $new_license;
12389 }
12390
12391 $this->_sync_site_subscription( $new_license );
12392
12393 return $this->_license;
12394 }
12395
12396 /**
12397 * @author Vova Feldman (@svovaf)
12398 * @since 2.3.1
12399 *
12400 * @param \FS_Plugin_License $license
12401 */
12402 private function set_license( $license = null ) {
12403 $this->_license = $license;
12404
12405 $this->maybe_update_whitelabel_flag( $license );
12406 }
12407
12408 /**
12409 * @author Leo Fajardo (@leorw)
12410 * @since 2.3.1
12411 *
12412 * @param FS_Plugin_License $license
12413 */
12414 private function maybe_update_whitelabel_flag( $license ) {
12415 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12416 $this->_storage->is_whitelabeled :
12417 false;
12418
12419 if ( is_object( $license ) ) {
12420 $license_user = self::_get_user_by_id( $license->user_id );
12421
12422 if ( ! is_object( $license_user ) ) {
12423 // If foreign license, do not update the `is_whitelabeled` flag.
12424 return;
12425 }
12426
12427 if ( $this->is_addon() ) {
12428 /**
12429 * Store the last license data to the parent's storage since it's needed only when showing the
12430 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12431 * iterate over the add-ons just to get the last license data.
12432 */
12433 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12434 } else {
12435 $this->store_last_activated_license_data( $license );
12436 }
12437
12438 if ( $license->is_whitelabeled ) {
12439 // Activated a developer license, data should be hidden.
12440 $is_whitelabeled = true;
12441 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12442 // The account owner activated a regular license key, no need to hide the data.
12443 $is_whitelabeled = false;
12444 }
12445 }
12446
12447 $this->_storage->is_whitelabeled = $is_whitelabeled;
12448
12449 // Reset the whitelabeled status after update.
12450 $this->is_whitelabeled = null;
12451 if ( $this->is_addon() ) {
12452 $parent_fs = $this->get_parent_instance();
12453
12454 if ( is_object( $parent_fs ) ) {
12455 $parent_fs->is_whitelabeled = null;
12456 }
12457 }
12458 }
12459
12460 /**
12461 * @author Leo Fajardo (@leorw)
12462 * @since 2.3.1
12463 *
12464 * @param FS_Plugin_License $license
12465 * @param FS_User $license_user
12466 */
12467 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12468 if ( ! is_object( $license_user ) ) {
12469 $this->_storage->last_license_key = md5( $license->secret_key );
12470 $this->_storage->last_license_user_id = null;
12471 } else {
12472 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12473 $this->_storage->last_license_user_id = $license_user->id;
12474 }
12475 }
12476
12477 /**
12478 * @author Leo Fajardo (@leorw)
12479 * @since 2.3.1
12480 *
12481 * @param bool $ignore_data_debug_mode
12482 *
12483 * @return bool
12484 */
12485 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12486 if ( true !== $this->_storage->is_whitelabeled ) {
12487 return false;
12488 } else if ( $ignore_data_debug_mode ) {
12489 return true;
12490 }
12491
12492 $fs = $this->is_addon() ?
12493 $this->get_parent_instance() :
12494 $this;
12495
12496 return ! $fs->is_data_debug_mode();
12497 }
12498
12499 /**
12500 * @author Leo Fajardo (@leorw)
12501 * @since 2.3.1
12502 *
12503 * @return number
12504 */
12505 function get_last_license_user_id() {
12506 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12507 $this->_storage->last_license_user_id :
12508 null;
12509 }
12510
12511 /**
12512 * @author Leo Fajardo (@leorw)
12513 * @since 2.3.1
12514 *
12515 * @param int $blog_id
12516 * @param bool $ignore_data_debug_mode
12517 *
12518 * @return bool
12519 */
12520 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12521 if ( ! is_null( $blog_id ) ) {
12522 $this->switch_to_blog( $blog_id );
12523 }
12524
12525 if ( ! is_null( $this->is_whitelabeled ) ) {
12526 $is_whitelabeled = $this->is_whitelabeled;
12527 } else {
12528 $is_whitelabeled = false;
12529
12530 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12531
12532 if ( ! $this->has_addons() ) {
12533 $is_whitelabeled = $is_whitelabeled_flag;
12534 } else if ( $is_whitelabeled_flag ) {
12535 $is_whitelabeled = true;
12536 } else {
12537 if ( $this->is_registered() || $this->is_premium() ) {
12538 $addon_ids = $this->get_updated_account_addons();
12539 } else {
12540 $addons = self::get_all_addons();
12541
12542 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12543 $addons[ $this->_plugin->id ] :
12544 array();
12545
12546 $addon_ids = array();
12547 foreach ( $plugin_addons as $addon ) {
12548 $addon_ids[] = $addon->id;
12549 }
12550 }
12551
12552 $installed_addons = $this->get_installed_addons();
12553 foreach ( $installed_addons as $fs_addon ) {
12554 $addon_ids[] = $fs_addon->get_id();
12555 }
12556
12557 if ( ! empty( $addon_ids ) ) {
12558 $addon_ids = array_unique( $addon_ids );
12559
12560 $is_network_level = (
12561 fs_is_network_admin() &&
12562 $this->is_network_active()
12563 );
12564
12565 foreach ( $addon_ids as $addon_id ) {
12566 $addon = $this->get_addon( $addon_id );
12567
12568 if ( ! is_object( $addon ) ) {
12569 continue;
12570 }
12571
12572 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
12573 $fs_addon = $this->is_addon_activated( $addon_id ) ?
12574 self::get_addon_instance( $addon_id ) :
12575 null;
12576
12577 $was_addon_network_activated = false;
12578
12579 if ( is_object( $fs_addon ) ) {
12580 $was_addon_network_activated = $fs_addon->is_network_active();
12581 } else if ( $is_network_level ) {
12582 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
12583 }
12584
12585 $network_delegated_connection = (
12586 $was_addon_network_activated &&
12587 $addon_storage->get( 'is_delegated_connection', false, true )
12588 );
12589
12590 if (
12591 $is_network_level &&
12592 ( ! $was_addon_network_activated || $network_delegated_connection )
12593 ) {
12594 $sites = self::get_sites();
12595
12596 /**
12597 * If in network admin area and the add-on was not network-activated or network-activated
12598 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
12599 */
12600 foreach ( $sites as $site ) {
12601 $site_info = $this->get_site_info( $site );
12602
12603 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
12604 $is_whitelabeled = true;
12605 break;
12606 }
12607 }
12608
12609 if ( $is_whitelabeled ) {
12610 break;
12611 }
12612 } else {
12613 /**
12614 * This will be executed when any of the following is met:
12615 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
12616 * 2. Add-on was network-activated, network-delegated, and in site admin area.
12617 * 3. Add-on was not network-activated and in site admin area.
12618 */
12619 if ( true === $addon_storage->is_whitelabeled ) {
12620 $is_whitelabeled = true;
12621 break;
12622 }
12623 }
12624 }
12625 }
12626 }
12627
12628 $this->is_whitelabeled = $is_whitelabeled;
12629
12630 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
12631 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
12632 }
12633
12634 if ( ! is_null( $blog_id ) ) {
12635 $this->restore_current_blog();
12636 }
12637 }
12638
12639 return (
12640 $is_whitelabeled &&
12641 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
12642 );
12643 }
12644
12645 /**
12646 * Sync site's subscription.
12647 *
12648 * @author Vova Feldman (@svovaf)
12649 * @since 1.0.9
12650 *
12651 * @param FS_Plugin_License|null $license
12652 *
12653 * @return bool|\FS_Subscription
12654 */
12655 private function _sync_site_subscription( $license ) {
12656 if ( ! is_object( $license ) ) {
12657 $this->delete_unused_subscriptions();
12658
12659 return false;
12660 }
12661
12662 // Load subscription details if not lifetime.
12663 $subscription = $license->is_lifetime() ?
12664 false :
12665 $this->_fetch_site_license_subscription();
12666
12667 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
12668 $this->store_subscription( $subscription );
12669 } else {
12670 $this->delete_unused_subscriptions();
12671 }
12672
12673 return $subscription;
12674 }
12675
12676 /**
12677 * @author Vova Feldman (@svovaf)
12678 * @since 1.0.6
12679 *
12680 * @return bool|\FS_Plugin_License
12681 */
12682 function _get_license() {
12683 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
12684 return $this->_license;
12685 }
12686
12687 return $this->_get_available_premium_license();
12688 }
12689
12690 /**
12691 * @param number $license_id
12692 *
12693 * @return null|\FS_Subscription
12694 */
12695 function _get_subscription( $license_id ) {
12696 if ( ! isset( $this->_storage->subscriptions ) ||
12697 empty( $this->_storage->subscriptions )
12698 ) {
12699 return null;
12700 }
12701
12702 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
12703 if ( $subscription->license_id == $license_id ) {
12704 return $subscription;
12705 }
12706 }
12707
12708 return null;
12709 }
12710
12711 /**
12712 * @author Leo Fajardo (@leorw)
12713 * @since 2.0.0
12714 *
12715 * @param FS_Subscription $subscription
12716 */
12717 function store_subscription( FS_Subscription $subscription ) {
12718 if ( ! isset( $this->_storage->subscriptions ) ) {
12719 $this->_storage->subscriptions = array();
12720 }
12721
12722 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
12723 $this->_storage->subscriptions = array( $subscription );
12724
12725 return;
12726 }
12727
12728 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
12729
12730 $updated_subscription = false;
12731 foreach ( $subscriptions as $key => $existing_subscription ) {
12732 if ( $existing_subscription->id == $subscription->id ) {
12733 $subscriptions[ $key ] = $subscription;
12734 $updated_subscription = true;
12735 break;
12736 }
12737 }
12738
12739 if ( ! $updated_subscription ) {
12740 $subscriptions[] = $subscription;
12741 }
12742
12743 $this->_storage->subscriptions = $subscriptions;
12744 }
12745
12746 /**
12747 * @author Leo Fajardo (@leorw)
12748 * @since 2.0.0
12749 */
12750 function delete_unused_subscriptions() {
12751 if ( ! isset( $this->_storage->subscriptions ) ||
12752 empty( $this->_storage->subscriptions ) ||
12753 // Clean up only if there are already at least 3 subscriptions.
12754 ( count( $this->_storage->subscriptions ) < 3 )
12755 ) {
12756 return;
12757 }
12758
12759 if ( ! is_multisite() ) {
12760 // If not multisite, there should only be 1 subscription, so just clear the array.
12761 $this->_storage->subscriptions = array();
12762
12763 return;
12764 }
12765
12766 $subscriptions_to_keep_by_license_id_map = array();
12767 $sites = self::get_sites();
12768 foreach ( $sites as $site ) {
12769 $blog_id = self::get_site_blog_id( $site );
12770 $install = $this->get_install_by_blog_id( $blog_id );
12771
12772 if ( ! is_object( $install ) ||
12773 ! FS_Plugin_License::is_valid_id( $install->license_id )
12774 ) {
12775 continue;
12776 }
12777
12778 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
12779 }
12780
12781 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
12782 $this->_storage->subscriptions = array();
12783
12784 return;
12785 }
12786
12787 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
12788 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
12789 unset( $this->_storage->subscriptions[ $key ] );
12790 }
12791 }
12792 }
12793
12794 /**
12795 * @author Vova Feldman (@svovaf)
12796 * @since 1.0.2
12797 *
12798 * @param string $plan Plan name
12799 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12800 *
12801 * @return bool
12802 */
12803 function is_plan( $plan, $exact = false ) {
12804 $this->_logger->entrance();
12805
12806 if ( ! $this->is_registered() ) {
12807 return false;
12808 }
12809
12810 $plan = strtolower( $plan );
12811
12812 $current_plan_name = $this->get_plan_name();
12813
12814 if ( $current_plan_name === $plan ) {
12815 // Exact plan.
12816 return true;
12817 } else if ( $exact ) {
12818 // Required exact, but plans are different.
12819 return false;
12820 }
12821
12822 $current_plan_order = - 1;
12823 $required_plan_order = PHP_INT_MAX;
12824 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12825 if ( $plan === $this->_plans[ $i ]->name ) {
12826 $required_plan_order = $i;
12827 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
12828 $current_plan_order = $i;
12829 }
12830 }
12831
12832 return ( $current_plan_order > $required_plan_order );
12833 }
12834
12835 /**
12836 * Check if module has only one plan.
12837 *
12838 * @author Vova Feldman (@svovaf)
12839 * @since 1.2.1.7
12840 *
12841 * @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.
12842 *
12843 * @return bool
12844 */
12845 function is_single_plan( $double_check = false ) {
12846 $this->_logger->entrance();
12847
12848 if ( ! $this->is_registered() ||
12849 ! is_array( $this->_plans ) ||
12850 0 === count( $this->_plans )
12851 ) {
12852 return true;
12853 }
12854
12855 $has_free_plan = $this->has_free_plan();
12856
12857 if ( ! $has_free_plan && $double_check ) {
12858 foreach ( $this->_plans as $plan ) {
12859 if ( $plan->is_free() ) {
12860 $has_free_plan = true;
12861 break;
12862 }
12863 }
12864 }
12865
12866 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
12867 }
12868
12869 /**
12870 * Check if plan based on trial. If not in trial mode, should return false.
12871 *
12872 * @since 1.0.9
12873 *
12874 * @param string $plan Plan name
12875 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
12876 *
12877 * @return bool
12878 */
12879 function is_trial_plan( $plan, $exact = false ) {
12880 $this->_logger->entrance();
12881
12882 if ( ! $this->is_registered() ) {
12883 return false;
12884 }
12885
12886 if ( ! $this->is_trial() ) {
12887 return false;
12888 }
12889
12890 $trial_plan = $this->get_trial_plan();
12891
12892 if ( $trial_plan->name === $plan ) {
12893 // Exact plan.
12894 return true;
12895 } else if ( $exact ) {
12896 // Required exact, but plans are different.
12897 return false;
12898 }
12899
12900 $current_plan_order = - 1;
12901 $required_plan_order = - 1;
12902 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12903 if ( $plan === $this->_plans[ $i ]->name ) {
12904 $required_plan_order = $i;
12905 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
12906 $current_plan_order = $i;
12907 }
12908 }
12909
12910 return ( $current_plan_order > $required_plan_order );
12911 }
12912
12913 /**
12914 * Check if plugin has any paid plans.
12915 *
12916 * @author Vova Feldman (@svovaf)
12917 * @since 1.0.7
12918 *
12919 * @return bool
12920 */
12921 function has_paid_plan() {
12922 return $this->_has_paid_plans ||
12923 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
12924 }
12925
12926 /**
12927 * Check if plugin has any plan with a trail.
12928 *
12929 * @author Vova Feldman (@svovaf)
12930 * @since 1.0.9
12931 *
12932 * @return bool
12933 */
12934 function has_trial_plan() {
12935 /**
12936 * @author Vova Feldman(@svovaf)
12937 * @since 1.2.1.5
12938 *
12939 * Allow setting a trial from the SDK without calling the API.
12940 * But, if the user did opt-in, continue using the real data from the API.
12941 */
12942 if ( $this->_trial_days >= 0 ) {
12943 return true;
12944 }
12945
12946 return $this->_storage->get( 'has_trial_plan', false );
12947 }
12948
12949 /**
12950 * Check if plugin has any free plan, or is it premium only.
12951 *
12952 * Note: If no plans configured, assume plugin is free.
12953 *
12954 * @author Vova Feldman (@svovaf)
12955 * @since 1.0.7
12956 *
12957 * @return bool
12958 */
12959 function has_free_plan() {
12960 return ! $this->is_only_premium();
12961 }
12962
12963 /**
12964 * Displays a license activation dialog box when the user clicks on the "Activate License"
12965 * or "Change License" link on the plugins
12966 * page.
12967 *
12968 * @author Leo Fajardo (@leorw)
12969 * @since 1.1.9
12970 */
12971 function _add_license_activation_dialog_box() {
12972 $vars = array(
12973 'id' => $this->_module_id,
12974 );
12975
12976 fs_require_template( 'forms/license-activation.php', $vars );
12977 fs_require_template( 'forms/resend-key.php', $vars );
12978 }
12979
12980 /**
12981 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
12982 *
12983 * @author Leo Fajardo (@leorw)
12984 * @since 2.5.0
12985 */
12986 function _add_email_address_update_dialog_box() {
12987 $vars = array( 'id' => $this->_module_id );
12988
12989 fs_require_template( 'forms/email-address-update.php', $vars );
12990 }
12991
12992 /**
12993 * @author Leo Fajardo (@leorw)
12994 * @since 2.5.0
12995 */
12996 function _add_email_address_update_option() {
12997 if ( ! $this->should_handle_user_change() ) {
12998 return;
12999 }
13000
13001 // Add email address update AJAX handler.
13002 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13003 }
13004
13005 /**
13006 * @author Leo Fajardo (@leorw)
13007 * @since 2.5.0
13008 */
13009 function _email_address_update_ajax_handler() {
13010 $this->check_ajax_referer( 'update_email_address' );
13011
13012 $new_email_address = fs_request_get( 'email_address' );
13013 $transfer_type = fs_request_get( 'transfer_type' );
13014
13015 $result = $this->update_email( $new_email_address );
13016
13017 if ( ! FS_Api::is_api_error( $result ) ) {
13018 self::shoot_ajax_success();
13019 }
13020
13021 $error = '';
13022
13023 if ( FS_Api::is_api_error_object( $result ) ) {
13024 switch ( $result->error->code ) {
13025 case 'user_exist':
13026 case 'account_verification_required':
13027 $error = array(
13028 'code' => 'change_ownership',
13029 'url' => $this->get_account_url( 'change_owner', array(
13030 'state' => 'init',
13031 'candidate_email' => $new_email_address,
13032 'transfer_type' => $transfer_type,
13033 ) ),
13034 );
13035
13036 break;
13037 }
13038 }
13039
13040 if ( empty( $error ) ) {
13041 $error = is_object( $result ) ?
13042 var_export( $result->error, true ) :
13043 $result;
13044 }
13045
13046 self::shoot_ajax_failure( $error );
13047 }
13048
13049 /**
13050 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13051 *
13052 * @author Leo Fajardo (@leorw)
13053 * @since 2.3.2
13054 *
13055 * @return number[]
13056 */
13057 function get_installs_ids_with_foreign_licenses() {
13058 $installs = array();
13059
13060 if (
13061 is_object( $this->_license ) &&
13062 $this->_site->user_id != $this->_license->user_id
13063 ) {
13064 $installs[] = $this->_site->id;
13065 }
13066
13067 /**
13068 * Also try to get foreign licenses for the context product's add-ons.
13069 */
13070 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13071
13072 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13073 if ( $slug == $this->get_slug() ) {
13074 continue;
13075 }
13076
13077 $install = $install_info['install'];
13078 $license = $install_info['license'];
13079
13080 if (
13081 is_object( $license ) &&
13082 $install->user_id != $license->user_id
13083 ) {
13084 $installs[] = $install->id;
13085 }
13086 }
13087
13088 return $installs;
13089 }
13090
13091 /**
13092 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13093 *
13094 * @author Leo Fajardo (@leorw)
13095 * @since 2.3.2
13096 *
13097 * @param number[] $install_ids
13098 */
13099 function _add_user_change_dialog_box( $install_ids ) {
13100 $vars = array(
13101 'id' => $this->_module_id,
13102 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13103 );
13104
13105 fs_require_template( 'forms/user-change.php', $vars );
13106 }
13107
13108 /**
13109 * @author Leo Fajardo (@leorw)
13110 * @since 2.3.1
13111 */
13112 function _add_data_debug_mode_dialog_box() {
13113 $vars = array(
13114 'id' => $this->_module_id,
13115 );
13116
13117 fs_require_template( 'forms/data-debug-mode.php', $vars );
13118 }
13119
13120 /**
13121 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13122 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13123 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13124 * is only activated on a single production site.
13125 *
13126 * @author Leo Fajardo (@leorw)
13127 * @since 2.2.1
13128 *
13129 * @param bool $is_license_deactivation
13130 *
13131 * @return array
13132 */
13133 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13134 if ( fs_is_network_admin() ) {
13135 // Subscription cancellation dialog box is currently not supported for multisite networks.
13136 return array();
13137 }
13138
13139 if ( $this->is_whitelabeled() ) {
13140 return array();
13141 }
13142
13143 $license = $this->_get_license();
13144
13145 /**
13146 * 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.
13147 *
13148 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13149 * @since 2.2.1
13150 */
13151 if ( ! is_object( $license ) ||
13152 $license->is_lifetime() ||
13153 ( ! $license->is_single_site() && $license->activated > 1 )
13154 ) {
13155 return array();
13156 }
13157
13158 /**
13159 * @var FS_Subscription $subscription
13160 */
13161 $subscription = $this->_get_subscription( $license->id );
13162 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13163 return array();
13164 }
13165
13166 return array(
13167 'id' => $this->_module_id,
13168 'license' => $license,
13169 'has_trial' => $this->is_paid_trial(),
13170 'is_license_deactivation' => $is_license_deactivation,
13171 );
13172 }
13173
13174 /**
13175 * @author Leo Fajardo (@leorw)
13176 * @since 2.0.2
13177 */
13178 function _add_premium_version_upgrade_selection_dialog_box() {
13179 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13180 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13181 return;
13182 }
13183
13184 $vars = array(
13185 'id' => $this->_module_id,
13186 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13187 $modules_update->response[ $this->_plugin_basename ]->new_version :
13188 $modules_update->response[ $this->_plugin_basename ]['new_version']
13189 );
13190
13191 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13192 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13193 }
13194
13195 /**
13196 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13197 * page.
13198 *
13199 * @author Leo Fajardo (@leorw)
13200 * @since 1.2.1.5
13201 */
13202 function _add_optout_dialog() {
13203 if ( $this->is_theme() ) {
13204 $vars = null;
13205 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13206 }
13207
13208 $vars = array( 'id' => $this->_module_id );
13209 fs_require_template( 'forms/optout.php', $vars );
13210 }
13211
13212 /**
13213 * Prepare page to include all required UI and logic for the license activation dialog.
13214 *
13215 * @author Vova Feldman (@svovaf)
13216 * @since 1.2.0
13217 */
13218 function _add_license_activation() {
13219 if ( $this->is_migration() ) {
13220 return;
13221 }
13222
13223 if ( ! $this->is_user_admin() ) {
13224 // Only admins can activate a license.
13225 return;
13226 }
13227
13228 if ( ! $this->has_paid_plan() ) {
13229 // Module doesn't have any paid plans.
13230 return;
13231 }
13232
13233 if (
13234 $this->has_premium_version() &&
13235 ! $this->is_premium() &&
13236 /**
13237 * Also handle the case when an upgrade was made using the free version.
13238 *
13239 * @author Leo Fajardo (@leorw)
13240 * @since 2.3.2
13241 */
13242 ! is_object( $this->_get_license() )
13243 ) {
13244 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13245 return;
13246 }
13247
13248 // Add license activation link and AJAX request handler.
13249 if ( self::is_plugins_page() ) {
13250 $is_network_admin = fs_is_network_admin();
13251
13252 if (
13253 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13254 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13255 ) {
13256 if (
13257 $this->is_premium() ||
13258 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13259 ) {
13260 /**
13261 * @since 1.2.0 Add license action link only on plugins page.
13262 */
13263 $this->_add_license_action_link();
13264 }
13265 }
13266 }
13267
13268 // Add license activation AJAX callback.
13269 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13270
13271 // Add resend license AJAX callback.
13272 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13273 }
13274
13275 /**
13276 * Prepares page to include all required UI and logic for the "Change User" dialog.
13277 *
13278 * @author Leo Fajardo (@leorw)
13279 * @since 2.3.2
13280 */
13281 function _add_user_change_option() {
13282 if ( ! $this->should_handle_user_change() ) {
13283 return;
13284 }
13285
13286 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13287
13288 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13289 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13290 return;
13291 }
13292
13293 // Add user change AJAX handler.
13294 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13295 }
13296
13297 /**
13298 * @author Leo Fajardo (@leorw)
13299 * @since 2.3.2
13300 */
13301 function should_handle_user_change() {
13302 if ( ! $this->is_user_admin() ) {
13303 // Only admins can change user.
13304 return false;
13305 }
13306
13307 if ( $this->is_addon() ) {
13308 return false;
13309 }
13310
13311 if ( ! $this->is_registered() ) {
13312 return false;
13313 }
13314
13315 if (
13316 $this->is_network_active() &&
13317 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13318 ) {
13319 // Handle only on site-level "Account" section for now.
13320 return false;
13321 }
13322
13323 return true;
13324 }
13325
13326 /**
13327 * @author Leo Fajardo (@leorw)
13328 * @since 2.0.2
13329 */
13330 function _add_premium_version_upgrade_selection() {
13331 if ( ! $this->is_user_admin() ) {
13332 return;
13333 }
13334
13335 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13336 // This is relevant only to the free versions and premium versions without an active license.
13337 return;
13338 }
13339
13340 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13341 $this->_add_premium_version_upgrade_selection_action();
13342 }
13343 }
13344
13345 /**
13346 * @author Edgar Melkonyan
13347 * @since 2.4.1
13348 *
13349 * @throws Freemius_Exception
13350 */
13351 function _toggle_whitelabel_mode_ajax_handler() {
13352 $this->_logger->entrance();
13353
13354 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13355
13356 if ( ! $this->is_user_admin() ) {
13357 // Only for admins.
13358 self::shoot_ajax_failure();
13359 }
13360
13361 $license = $this->get_api_user_scope()->call(
13362 "/licenses/{$this->_site->license_id}.json",
13363 'put',
13364 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13365 );
13366
13367 if ( ! $this->is_api_result_entity( $license ) ) {
13368 self::shoot_ajax_failure(
13369 FS_Api::is_api_error_object( $license ) ?
13370 $license->error->message :
13371 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13372 );
13373 }
13374
13375 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13376 $this->_store_licenses();
13377
13378 $this->_sync_license();
13379
13380 if ( ! $license->is_whitelabeled ) {
13381 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13382 } else {
13383 $this->_admin_notices->add_sticky(
13384 sprintf(
13385 $this->get_text_inline(
13386 'Your %s license was flagged as white-labeled to hide sensitive information from the WP Admin (e.g. your email, license key, prices, billing address & invoices). If you ever wish to revert it back, you can easily do it through your %s. If this was a mistake you can also %s.',
13387 'license_whitelabeled'
13388 ),
13389 "<strong>{$this->get_plugin_title()}</strong>",
13390 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13391 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13392 ),
13393 'license_whitelabeled'
13394 );
13395 }
13396
13397 self::shoot_ajax_response( array( 'success' => true ) );
13398 }
13399
13400 /**
13401 * @author Leo Fajardo (@leorw)
13402 * @since 2.3.0
13403 */
13404 function _add_beta_mode_update_handler() {
13405 if ( ! $this->is_user_admin() ) {
13406 return;
13407 }
13408
13409 if ( ! $this->is_premium() ) {
13410 return;
13411 }
13412
13413 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13414 }
13415
13416 /**
13417 * @author Leo Fajardo (@leorw)
13418 * @since 2.3.0
13419 */
13420 function _set_beta_mode_ajax_handler() {
13421 $this->_logger->entrance();
13422
13423 $this->check_ajax_referer( 'set_beta_mode' );
13424
13425 if ( ! $this->is_user_admin() ) {
13426 // Only for admins.
13427 self::shoot_ajax_failure();
13428 }
13429
13430 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13431
13432 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13433 self::shoot_ajax_failure();
13434 }
13435
13436 $site = $this->api_site_call(
13437 '',
13438 'put',
13439 array(
13440 'is_beta' => ( 'true' == $is_beta ),
13441 'fields' => 'is_beta'
13442 )
13443 );
13444
13445 if ( ! $this->is_api_result_entity( $site ) ) {
13446 self::shoot_ajax_failure(
13447 FS_Api::is_api_error_object( $site ) ?
13448 $site->error->message :
13449 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13450 );
13451 }
13452
13453 $this->_site->is_beta = $site->is_beta;
13454 $this->_store_site();
13455
13456 self::shoot_ajax_response( array( 'success' => true ) );
13457 }
13458
13459 /**
13460 * License activation WP AJAX handler.
13461 *
13462 * @author Leo Fajardo (@leorw)
13463 * @since 1.1.9
13464 *
13465 * @uses Freemius::activate_license()
13466 */
13467 function _activate_license_ajax_action() {
13468 $this->_logger->entrance();
13469
13470 $this->check_ajax_referer( 'activate_license' );
13471
13472 $license_key = trim( fs_request_get_raw( 'license_key' ) );
13473
13474 if ( empty( $license_key ) ) {
13475 $license_id = trim( fs_request_get_raw( 'license_id' ) );
13476
13477 if ( FS_Plugin_License::is_valid_id( $license_id ) ) {
13478 $license = $this->_get_license_by_id( $license_id, false );
13479
13480 if ( is_object( $license ) ) {
13481 $license_key = $license->secret_key;
13482 }
13483 }
13484 }
13485
13486 if ( empty( $license_key ) ) {
13487 exit;
13488 }
13489
13490 $sites = fs_is_network_admin() ?
13491 fs_request_get( 'sites', array(), 'post' ) :
13492 array();
13493
13494 $result = $this->activate_license(
13495 $license_key,
13496 $sites,
13497 fs_request_get_bool( 'is_marketing_allowed', null ),
13498 fs_request_get( 'blog_id', null ),
13499 fs_request_get( 'module_id', null, 'post' ),
13500 fs_request_get( 'user_id', null ),
13501 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13502 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
13503 fs_request_get( 'license_owner_id', null )
13504 );
13505
13506 if (
13507 $result['success'] &&
13508 $this->is_bundle_license_auto_activation_enabled()
13509 ) {
13510 $license = new FS_Plugin_License();
13511 $license->secret_key = $license_key;
13512
13513 $this->maybe_activate_bundle_license( $license, $sites );
13514 }
13515
13516 echo json_encode( $result );
13517
13518 exit;
13519 }
13520
13521 /**
13522 * User change WP AJAX handler.
13523 *
13524 * @author Leo Fajardo (@leorw)
13525 * @since 2.3.2
13526 */
13527 function _user_change_ajax_action() {
13528 $this->_logger->entrance();
13529
13530 $this->check_ajax_referer( 'change_user' );
13531
13532 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13533 $new_user_id = fs_request_get( 'user_id' );
13534
13535 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13536 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13537 }
13538
13539 $params = array();
13540
13541 if ( ! empty( $new_email_address ) ) {
13542 $params['user_email'] = $new_email_address;
13543 } else {
13544 $params['user_id'] = $new_user_id;
13545 }
13546
13547 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13548 $install_ids = array();
13549
13550 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13551 $install_ids[ $slug ] = $install_info['install']->id;
13552 }
13553
13554 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13555
13556 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13557
13558 if ( FS_Api::is_api_error( $install ) ) {
13559 $error = '';
13560
13561 if ( is_object( $install ) ) {
13562 switch ( $install->error->code ) {
13563 case 'user_exist':
13564 $error = (
13565 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13566 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
13567 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>' ) .
13568 sprintf(
13569 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13570 $this->get_account_url( 'change_owner', array(
13571 'state' => 'init',
13572 'candidate_email' => $new_email_address
13573 ) ),
13574 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
13575 )
13576 );
13577 break;
13578 }
13579 }
13580
13581 if ( empty( $error ) ) {
13582 $error = FS_Api::is_api_error_object( $install ) ?
13583 $install->error->message :
13584 var_export( $install->error, true );
13585 }
13586
13587 self::shoot_ajax_failure( $error );
13588 } else {
13589 if (
13590 // If successful ownership change.
13591 $this->get_user()->id != $install->user_id ||
13592 ! empty( $new_email_address )
13593 ) {
13594 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
13595 }
13596 }
13597
13598 self::shoot_ajax_success();
13599 }
13600
13601 /**
13602 * @author Leo Fajardo (@leorw)
13603 * @since 2.3.2.14
13604 */
13605 function starting_migration() {
13606 if ( ! empty( $this->_storage->license_migration ) ) {
13607 // Do not overwrite the data if already set.
13608 return;
13609 }
13610
13611 $this->_storage->license_migration = array(
13612 'is_migrating' => true,
13613 'start_timestamp' => time()
13614 );
13615 }
13616
13617 /**
13618 * @author Leo Fajardo (@leorw)
13619 * @since 2.3.2.14
13620 */
13621 function is_migration() {
13622 if ( $this->is_addon() ) {
13623 return $this->get_parent_instance()->is_migration();
13624 }
13625
13626 if ( empty( $this->_storage->license_migration ) ) {
13627 return false;
13628 }
13629
13630 if ( ! $this->_storage->license_migration['is_migrating'] ) {
13631 return false;
13632 }
13633
13634 return (
13635 // Return `true` if the migration is within 5 minutes from the starting time.
13636 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
13637 );
13638 }
13639
13640 /**
13641 *
13642 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
13643 *
13644 * @author Vova Feldman (@svovaf)
13645 * @since 2.3.0
13646 *
13647 * @param string $license_key
13648 * @param null|bool $is_marketing_allowed
13649 * @param null|number $plugin_id
13650 * @param array $sites
13651 * @param int $blog_id
13652 * @param null|number $license_owner_id
13653 *
13654 * @return array {
13655 * @var bool $success
13656 * @var string $error
13657 * @var string $next_page
13658 * }
13659 *
13660 * @uses Freemius::activate_license()
13661 */
13662 function activate_migrated_license(
13663 $license_key,
13664 $is_marketing_allowed = null,
13665 $plugin_id = null,
13666 $sites = array(),
13667 $blog_id = null,
13668 $license_owner_id = null
13669 ) {
13670 $this->_logger->entrance();
13671
13672 $result = $this->activate_license(
13673 $license_key,
13674 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
13675 $this->get_sites_for_network_level_optin() :
13676 $sites,
13677 $is_marketing_allowed,
13678 $blog_id,
13679 $plugin_id,
13680 null,
13681 null,
13682 null,
13683 $license_owner_id
13684 );
13685
13686 // No need to show the sticky after license activation notice after migrating a license.
13687 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
13688
13689 return $result;
13690 }
13691
13692 /**
13693 * @author Leo Fajardo (@leorw)
13694 * @since 2.3.1
13695 *
13696 * @return string
13697 */
13698 function get_pricing_js_path() {
13699 if ( ! isset( $this->_pricing_js_path ) ) {
13700 $default_path = WP_FS__DIR_JS . '/pricing/freemius-pricing.js';
13701
13702 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', $default_path );
13703
13704 // Backward compatibility for people who placed the freemius-pricing inside `includes` directory. Let it take more preference than the default path.
13705 if ( empty( $pricing_js_path ) ) {
13706 global $fs_active_plugins;
13707
13708 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
13709 if ( $data->plugin_path == $this->get_plugin_basename() ) {
13710 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
13711
13712 $pricing_js_path = $plugin_or_theme_root_dir
13713 . '/'
13714 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
13715 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
13716 . '/includes/freemius-pricing/freemius-pricing.js';
13717
13718 break;
13719 }
13720 }
13721 }
13722
13723 // If it is still empty, load the default pricing JS.
13724 if ( ! file_exists( $pricing_js_path ) ) {
13725 $pricing_js_path = $default_path;
13726 }
13727
13728 $this->_pricing_js_path = $pricing_js_path;
13729 }
13730
13731 return $this->_pricing_js_path;
13732 }
13733
13734 /**
13735 * @author Leo Fajardo (@leorw)
13736 * @since 2.3.1
13737 *
13738 * @deprecated Since v2.9.0 we have removed the iFrame based pricing. This will always return `false`.
13739 *
13740 * @return bool
13741 */
13742 function should_use_external_pricing() {
13743 return false;
13744 }
13745
13746 /**
13747 * The implementation of this method was previously in `_activate_license_ajax_action()`.
13748 *
13749 * @author Vova Feldman (@svovaf)
13750 * @since 2.2.4
13751 * @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).
13752 * @param string $license_key
13753 * @param array $sites
13754 * @param null|bool $is_marketing_allowed
13755 * @param null|int $blog_id
13756 * @param null|number $plugin_id
13757 * @param null|number $user_id
13758 * @param bool|null $is_extensions_tracking_allowed
13759 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
13760 * @param null|number $license_owner_id
13761 *
13762 *
13763 * @return array {
13764 * @var bool $success
13765 * @var string $error
13766 * @var string $next_page
13767 * }
13768 */
13769 private function activate_license(
13770 $license_key,
13771 $sites = array(),
13772 $is_marketing_allowed = null,
13773 $blog_id = null,
13774 $plugin_id = null,
13775 $user_id = null,
13776 $is_extensions_tracking_allowed = null,
13777 $is_diagnostic_tracking_allowed = null,
13778 $license_owner_id = null
13779 ) {
13780 $this->_logger->entrance();
13781
13782 $license_key = trim( $license_key );
13783
13784 $is_network_activation_or_migration = (
13785 fs_is_network_admin() ||
13786 ( ! empty( $sites ) && $this->is_migration() )
13787 );
13788
13789 if ( ! $is_network_activation_or_migration ) {
13790 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
13791 $sites = array();
13792 }
13793
13794 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
13795 $this :
13796 $this->get_addon_instance( $plugin_id );
13797
13798 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
13799 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
13800 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
13801 ) );
13802
13803 $error = false;
13804 $next_page = false;
13805
13806 $has_valid_blog_id = is_numeric( $blog_id );
13807
13808 $user = null;
13809
13810 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
13811 /**
13812 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
13813 *
13814 * @author Vova Feldman (@svovaf)
13815 */
13816 $user = $fs->get_parent_instance()->get_current_or_network_user();
13817 } else if ( $fs->is_registered() ) {
13818 $user = $fs->get_current_or_network_user();
13819 }
13820
13821 if ( $has_valid_blog_id ) {
13822 /**
13823 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
13824 *
13825 * @author Leo Fajardo (@leorw)
13826 */
13827 $fs->switch_to_blog( $blog_id );
13828 }
13829
13830 if ( is_object( $user ) ) {
13831 $result = true;
13832
13833 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
13834 // If no specific blog ID was provided, activate the license for all sites in the network.
13835 $blog_2_install_map = array();
13836 $site_ids = array();
13837
13838 foreach ( $sites as $site ) {
13839 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
13840 continue;
13841 }
13842
13843 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
13844
13845 if ( is_object( $install ) ) {
13846 $blog_2_install_map[ $site['blog_id'] ] = $install;
13847 } else {
13848 $site_ids[] = $site['blog_id'];
13849 }
13850 }
13851
13852 if ( ! empty( $blog_2_install_map ) ) {
13853 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
13854 }
13855
13856 if ( true === $result && ! empty( $site_ids ) ) {
13857 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
13858 }
13859 } else {
13860 if ( $fs->is_registered() ) {
13861 $params = array(
13862 'license_key' => $fs->apply_filters( 'license_key', $license_key )
13863 );
13864
13865 $install_ids = array();
13866
13867 $change_owner = FS_User::is_valid_id( $user_id );
13868
13869 if ( $change_owner ) {
13870 $params['user_id'] = $user_id;
13871
13872 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
13873
13874 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13875 $install_ids[ $slug ] = $install_info['install']->id;
13876 }
13877
13878 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13879 }
13880
13881 $api = $fs->get_api_site_scope();
13882
13883 $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
13884
13885 if ( ! FS_Api::is_api_error( $result ) ) {
13886 $install = $result;
13887
13888 $fs->reconnect_locally( $has_valid_blog_id );
13889
13890 if (
13891 $change_owner &&
13892 // If successful ownership change.
13893 $fs->get_user()->id != $install->user_id
13894 ) {
13895 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
13896 }
13897 }
13898 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
13899 $result = $fs->activate_license_on_site( $user, $license_key );
13900 }
13901 }
13902
13903 $is_connected = null;
13904
13905 if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) {
13906 if ( FS_Api::is_blocked( $result ) ) {
13907 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
13908
13909 $is_connected = false;
13910 }
13911
13912 $error = FS_Api::is_api_error_object( $result ) ?
13913 $result->error->message :
13914 var_export( $result, true );
13915 } else {
13916 $is_connected = true;
13917
13918 $fs->network_upgrade_mode_completed();
13919
13920 $fs->_user = $user;
13921
13922 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
13923 $fs->_site = $fs->get_network_install();
13924 }
13925
13926 $fs->_sync_license( true, $has_valid_blog_id );
13927
13928 $this->maybe_sync_install_user();
13929
13930 $next_page = $fs->is_addon() ?
13931 $fs->get_parent_instance()->get_account_url() :
13932 $fs->get_after_activation_url( 'after_connect_url' );
13933 }
13934
13935 $fs->update_connectivity_info( $is_connected );
13936 } else {
13937 $next_page = $fs->opt_in(
13938 false,
13939 false,
13940 false,
13941 $license_key,
13942 false,
13943 false,
13944 false,
13945 $is_marketing_allowed,
13946 $sites,
13947 true,
13948 $license_owner_id
13949 );
13950
13951 if ( isset( $next_page->error ) ) {
13952 $error = $next_page->error;
13953 } else {
13954 if ( $is_network_activation_or_migration ) {
13955 /**
13956 * Get the list of sites that were just opted-in (and license activated).
13957 * This is an optimization for the next part below saving some DB queries.
13958 */
13959 $connected_sites = array();
13960 foreach ( $sites as $site ) {
13961 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
13962 $connected_sites[ $site['blog_id'] ] = true;
13963 }
13964 }
13965
13966 $all_sites = self::get_sites();
13967 $pending_blog_ids = array();
13968
13969 /**
13970 * 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.
13971 *
13972 * @author Vova Feldman (@svovaf)
13973 */
13974 foreach ( $all_sites as $site ) {
13975 $blog_id = self::get_site_blog_id( $site );
13976
13977 if ( isset( $connected_sites[ $blog_id ] ) ) {
13978 // Site was just connected.
13979 continue;
13980 }
13981
13982 if ( $fs->is_installed_on_site( $blog_id ) ) {
13983 // Site was already connected before.
13984 continue;
13985 }
13986
13987 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
13988 // Site's connection was delegated.
13989 continue;
13990 }
13991
13992 if ( $fs->is_anonymous_site( $blog_id ) ) {
13993 // Site connection was already skipped.
13994 continue;
13995 }
13996
13997 $pending_blog_ids[] = $blog_id;
13998 }
13999
14000 if ( ! empty( $pending_blog_ids ) ) {
14001 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14002 $fs->skip_connection( $pending_blog_ids );
14003 } else {
14004 $fs->delegate_connection( $pending_blog_ids );
14005 }
14006 }
14007 }
14008 }
14009 }
14010
14011 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14012 $fs->_storage->require_license_activation = false;
14013 }
14014
14015 $result = array(
14016 'success' => ( false === $error )
14017 );
14018
14019 if ( false !== $error ) {
14020 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14021 } else {
14022 if ( $fs->is_addon() || $fs->has_addons() ) {
14023 /**
14024 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14025 * an updated valid user licenses collection will be fetched from the server which is used to also
14026 * update the account add-ons (add-ons the user has licenses for).
14027 *
14028 * @author Leo Fajardo (@leorw)
14029 * @since 2.2.4
14030 */
14031 $fs->purge_valid_user_licenses_cache();
14032 }
14033
14034 $result['next_page'] = $next_page;
14035 }
14036
14037 return $result;
14038 }
14039
14040 /**
14041 * @author Leo Fajardo (@leorw)
14042 * @since 2.3.2
14043 *
14044 * @return array {
14045 * @key string Product slug.
14046 * @value array {
14047 * @property FS_Site $site
14048 * @property FS_Plugin_License $license
14049 * }
14050 * }
14051 */
14052 private function get_parent_and_addons_installs_info() {
14053 $fs = $this->is_addon() ?
14054 $this->get_parent_instance() :
14055 $this;
14056
14057 $installed_addons_ids = array();
14058
14059 $installed_addons_instances = $fs->get_installed_addons();
14060 foreach ( $installed_addons_instances as $instance ) {
14061 $installed_addons_ids[] = $instance->get_id();
14062 }
14063
14064 $addons_ids = array_unique( array_merge(
14065 $installed_addons_ids,
14066 $fs->get_updated_account_addons()
14067 ) );
14068
14069 // Add parent product info.
14070 $installs_info_by_slug_map = array(
14071 $fs->get_slug() => array(
14072 'install' => $fs->get_site(),
14073 'license' => $fs->_get_license()
14074 )
14075 );
14076
14077 foreach ( $addons_ids as $addon_id ) {
14078 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14079
14080 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14081
14082 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14083 // Add-on is not associated with an install entity.
14084 continue;
14085 }
14086
14087 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14088 'install' => $addon_info['site'],
14089 'license' => isset( $addon_info['license'] ) ?
14090 $addon_info['license'] :
14091 null
14092 );
14093 }
14094
14095 return $installs_info_by_slug_map;
14096 }
14097
14098 /**
14099 * @author Leo Fajardo (@leorw)
14100 * @since 1.2.3.1
14101 */
14102 function _network_activate_ajax_action() {
14103 $this->_logger->entrance();
14104
14105 $this->check_ajax_referer( 'network_activate' );
14106
14107 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14108 $fs = ( $plugin_id == $this->_module_id ) ?
14109 $this :
14110 $this->get_addon_instance( $plugin_id );
14111
14112 $error = false;
14113
14114 $sites = fs_request_get( 'sites', array(), 'post' );
14115 if ( is_array( $sites ) && ! empty( $sites ) ) {
14116 $sites_by_action = array(
14117 'allow' => array(),
14118 'delegate' => array(),
14119 'skip' => array()
14120 );
14121
14122 foreach ( $sites as $site ) {
14123 $sites_by_action[ $site['action'] ][] = $site;
14124 }
14125
14126 $total_sites = count( $sites );
14127 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14128
14129 $next_page = '';
14130
14131 $has_any_install = fs_request_get_bool( 'has_any_install' );
14132
14133 if ( $total_sites === $total_sites_to_delegate &&
14134 ! $this->is_network_upgrade_mode() &&
14135 ! $has_any_install
14136 ) {
14137 $this->delegate_connection();
14138 } else {
14139 if ( ! empty( $sites_by_action['delegate'] ) ) {
14140 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14141 }
14142
14143 if ( ! empty( $sites_by_action['skip'] ) ) {
14144 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14145 }
14146
14147 if ( empty( $sites_by_action['allow'] ) ) {
14148 if ( $has_any_install ) {
14149 $first_install = $fs->find_first_install();
14150
14151 if ( ! is_null( $first_install ) ) {
14152 $fs->_site = $first_install['install'];
14153 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14154
14155 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14156 $fs->_storage->network_user_id = $fs->_user->id;
14157 }
14158 }
14159 } else {
14160 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14161 $next_page = $fs->opt_in(
14162 false,
14163 false,
14164 false,
14165 false,
14166 false,
14167 false,
14168 false,
14169 fs_request_get_bool( 'is_marketing_allowed', null ),
14170 $sites_by_action['allow']
14171 );
14172 } else {
14173 $next_page = $fs->install_with_user(
14174 $this->get_network_user(),
14175 false,
14176 false,
14177 false,
14178 true,
14179 $sites_by_action['allow']
14180 );
14181 }
14182
14183 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14184 $error = $next_page->error;
14185 }
14186 }
14187 }
14188
14189 if ( empty( $next_page ) ) {
14190 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14191 }
14192 } else {
14193 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14194 }
14195
14196 $result = array(
14197 'success' => ( false === $error )
14198 );
14199
14200 if ( false !== $error ) {
14201 $result['error'] = $error;
14202 } else {
14203 $result['next_page'] = $next_page;
14204 }
14205
14206 echo json_encode( $result );
14207
14208 exit;
14209 }
14210
14211 /**
14212 * Billing update AJAX callback.
14213 *
14214 * @author Vova Feldman (@svovaf)
14215 * @since 1.2.1.5
14216 */
14217 function _update_billing_ajax_action() {
14218 $this->_logger->entrance();
14219
14220 $this->check_ajax_referer( 'update_billing' );
14221
14222 if ( ! $this->is_user_admin() ) {
14223 // Only for admins.
14224 self::shoot_ajax_failure();
14225 }
14226
14227 $billing = fs_request_get( 'billing' );
14228
14229 $api = $this->get_api_user_scope();
14230 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14231 'plugin_id' => $this->get_parent_id(),
14232 ) ) );
14233
14234 if ( ! $this->is_api_result_entity( $result ) ) {
14235 self::shoot_ajax_failure();
14236 }
14237
14238 // Purge cached billing.
14239 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14240
14241 self::shoot_ajax_success();
14242 }
14243
14244 /**
14245 * Trial start for anonymous users (AJAX callback).
14246 *
14247 * @author Vova Feldman (@svovaf)
14248 * @since 1.2.1.5
14249 */
14250 function _start_trial_ajax_action() {
14251 $this->_logger->entrance();
14252
14253 $this->check_ajax_referer( 'start_trial' );
14254
14255 if ( ! $this->is_user_admin() ) {
14256 // Only for admins.
14257 self::shoot_ajax_failure();
14258 }
14259
14260 $trial_data = fs_request_get( 'trial' );
14261
14262 $next_page = $this->opt_in(
14263 false,
14264 false,
14265 false,
14266 false,
14267 false,
14268 $trial_data['plan_id']
14269 );
14270
14271 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14272 self::shoot_ajax_failure(
14273 isset( $next_page->error ) ?
14274 $next_page->error->message :
14275 var_export( $next_page, true )
14276 );
14277 }
14278
14279 $this->shoot_ajax_success( array(
14280 'next_page' => $next_page,
14281 ) );
14282 }
14283
14284 /**
14285 * @author Leo Fajardo (@leorw)
14286 * @since 1.2.0
14287 */
14288 function _resend_license_key_ajax_action() {
14289 $this->_logger->entrance();
14290
14291 $this->check_ajax_referer( 'resend_license_key' );
14292
14293 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14294
14295 if ( empty( $email_address ) ) {
14296 exit;
14297 }
14298
14299 $error = false;
14300
14301 $api = $this->get_api_plugin_scope();
14302 $result = $api->call( '/licenses/resend.json', 'post',
14303 array(
14304 'email' => $email_address,
14305 'url' => home_url(),
14306 )
14307 );
14308
14309 if ( is_object( $result ) && isset( $result->error ) ) {
14310 $error = $result->error;
14311
14312 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14313 $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' );
14314 } else if ( 'no_license' === $error->code ) {
14315 $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' );
14316 } else {
14317 $error = $error->message;
14318 }
14319 }
14320
14321 $licenses = array(
14322 'success' => ( false === $error )
14323 );
14324
14325 if ( false !== $error ) {
14326 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14327 }
14328
14329 echo json_encode( $licenses );
14330
14331 exit;
14332 }
14333
14334 /**
14335 * @author Vova Feldman (@svovaf)
14336 * @since 1.2.1.8
14337 *
14338 * @var string
14339 */
14340 private static $_pagenow;
14341
14342 /**
14343 * Get current page or the referer if executing a WP AJAX request.
14344 *
14345 * @author Vova Feldman (@svovaf)
14346 * @since 1.2.1.8
14347 *
14348 * @return string
14349 */
14350 static function get_current_page() {
14351 if ( ! isset( self::$_pagenow ) ) {
14352 global $pagenow;
14353 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14354 /**
14355 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14356 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14357 *
14358 * @author Leo Fajardo (@leorw)
14359 * @since 2.2.3
14360 */
14361 if ( is_network_admin() ) {
14362 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14363 } else if ( is_user_admin() ) {
14364 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14365 } else {
14366 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14367 }
14368
14369 $pagenow = $self_matches[1];
14370 $pagenow = trim( $pagenow, '/' );
14371 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14372 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14373 $pagenow = 'index.php';
14374 } else {
14375 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14376 $pagenow = strtolower( $self_matches[1] );
14377 if ( '.php' !== substr($pagenow, -4, 4) )
14378 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14379 }
14380 }
14381
14382 self::$_pagenow = $pagenow;
14383
14384 if ( self::is_ajax() &&
14385 'admin-ajax.php' === $pagenow
14386 ) {
14387 $referer = fs_get_raw_referer();
14388
14389 if ( is_string( $referer ) ) {
14390 $parts = explode( '?', $referer );
14391
14392 self::$_pagenow = basename( $parts[0] );
14393 }
14394 }
14395 }
14396
14397 return self::$_pagenow;
14398 }
14399
14400 /**
14401 * Helper method to check if user in the plugins page.
14402 *
14403 * @author Vova Feldman (@svovaf)
14404 * @since 1.2.1.5
14405 *
14406 * @return bool
14407 */
14408 static function is_plugins_page() {
14409 return ( 'plugins.php' === self::get_current_page() );
14410 }
14411
14412 /**
14413 * @author Leo Fajardo (@leorw)
14414 * @since 2.2.3
14415 *
14416 * @return bool
14417 */
14418 static function is_plugin_install_page() {
14419 return ( 'plugin-install.php' === self::get_current_page() );
14420 }
14421
14422 /**
14423 * @author Leo Fajardo (@leorw)
14424 * @since 2.0.2
14425 *
14426 * @return bool
14427 */
14428 static function is_updates_page() {
14429 return ( 'update-core.php' === self::get_current_page() );
14430 }
14431
14432 /**
14433 * Helper method to check if user in the themes page.
14434 *
14435 * @author Vova Feldman (@svovaf)
14436 * @since 1.2.2.6
14437 *
14438 * @return bool
14439 */
14440 static function is_themes_page() {
14441 return ( 'themes.php' === self::get_current_page() );
14442 }
14443
14444 #----------------------------------------------------------------------------------
14445 #region Affiliation
14446 #----------------------------------------------------------------------------------
14447
14448 /**
14449 * @author Leo Fajardo (@leorw)
14450 * @since 1.2.3
14451 *
14452 * @return bool
14453 */
14454 function has_affiliate_program() {
14455 if ( ! is_object( $this->_plugin ) ) {
14456 return false;
14457 }
14458
14459 return $this->_plugin->has_affiliate_program();
14460 }
14461
14462 /**
14463 * Get Plugin ID under which we will track affiliate application.
14464 *
14465 * This could either be the Bundle ID or the main plugin ID.
14466 *
14467 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14468 */
14469 private function get_plugin_id_for_affiliate_terms() {
14470 return $this->has_bundle_context() ?
14471 $this->get_bundle_id() :
14472 $this->_plugin->id;
14473 }
14474
14475 /**
14476 * @author Leo Fajardo (@leorw)
14477 * @since 1.2.4
14478 */
14479 private function fetch_affiliate_terms() {
14480 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14481 /**
14482 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14483 */
14484 $plugins_api = $this->has_bundle_context() ?
14485 $this->get_api_bundle_scope() :
14486 $this->get_api_plugin_scope();
14487
14488 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14489
14490 /**
14491 * At this point, we intentionally don't fallback to the main plugin, because the developer has chosen to use bundle. So it makes sense the affiliate program should be in context to the bundle too.
14492 */
14493 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14494 return;
14495 }
14496
14497 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14498 }
14499 }
14500
14501 /**
14502 * @author Leo Fajardo (@leorw)
14503 * @since 1.2.4
14504 */
14505 private function fetch_affiliate_and_custom_terms() {
14506 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14507 $application_data = $this->_storage->affiliate_application_data;
14508 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14509
14510 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14511
14512 $users_api = $this->get_api_user_scope();
14513 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14514 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14515 if ( ! empty( $result->affiliates ) ) {
14516 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14517
14518 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14519 $application_data['status'] = $affiliate->status;
14520 $this->_storage->affiliate_application_data = $application_data;
14521 }
14522
14523 if ( $affiliate->is_using_custom_terms ) {
14524 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14525 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14526 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14527 }
14528 }
14529
14530 $this->affiliate = $affiliate;
14531 }
14532 }
14533 }
14534 }
14535
14536 /**
14537 * @author Leo Fajardo (@leorw)
14538 * @since 1.2.3
14539 */
14540 private function fetch_affiliate_and_terms() {
14541 $this->_logger->entrance();
14542
14543 $this->fetch_affiliate_terms();
14544 $this->fetch_affiliate_and_custom_terms();
14545 }
14546
14547 /**
14548 * @author Leo Fajardo (@leorw)
14549 * @since 1.2.3
14550 *
14551 * @return FS_Affiliate
14552 */
14553 function get_affiliate() {
14554 return $this->affiliate;
14555 }
14556
14557
14558 /**
14559 * @author Leo Fajardo (@leorw)
14560 * @since 1.2.3
14561 *
14562 * @return FS_AffiliateTerms
14563 */
14564 function get_affiliate_terms() {
14565 return is_object( $this->custom_affiliate_terms ) ?
14566 $this->custom_affiliate_terms :
14567 $this->plugin_affiliate_terms;
14568 }
14569
14570 /**
14571 * @author Leo Fajardo (@leorw)
14572 * @since 1.2.3
14573 */
14574 function _submit_affiliate_application() {
14575 $this->_logger->entrance();
14576
14577 $this->check_ajax_referer( 'submit_affiliate_application' );
14578
14579 if ( ! $this->is_user_admin() ) {
14580 // Only for admins.
14581 self::shoot_ajax_failure();
14582 }
14583
14584 $affiliate = fs_request_get( 'affiliate' );
14585
14586 if ( empty( $affiliate['promotion_methods'] ) ) {
14587 unset( $affiliate['promotion_methods'] );
14588 }
14589
14590 if ( ! empty( $affiliate['additional_domains'] ) ) {
14591 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
14592 }
14593
14594 if ( ! $this->is_registered() ) {
14595 $email_address = isset( $affiliate['email'] ) ? $affiliate['email'] : '';
14596
14597 if ( ! is_email( $email_address ) ) {
14598 self::shoot_ajax_failure('Invalid email address.');
14599 }
14600
14601 // Opt in but don't track usage.
14602 $next_page = $this->opt_in(
14603 $email_address,
14604 false,
14605 false,
14606 false,
14607 false,
14608 false,
14609 true
14610 );
14611
14612 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14613 self::shoot_ajax_failure(
14614 isset( $next_page->error ) ?
14615 $next_page->error->message :
14616 var_export( $next_page, true )
14617 );
14618 } else if ( $this->is_pending_activation() ) {
14619 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation. Please check your email and click the link to activate your account and then submit the affiliate form again.', 'account-is-pending-activation' ) );
14620 }
14621 }
14622
14623 $this->fetch_affiliate_terms();
14624
14625 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14626
14627 $api = $this->get_api_user_scope();
14628 $result = $api->call(
14629 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
14630 'post',
14631 $affiliate
14632 );
14633
14634 if ( $this->is_api_error( $result ) ) {
14635 self::shoot_ajax_failure(
14636 isset( $result->error ) ?
14637 $result->error->message :
14638 var_export( $result, true )
14639 );
14640 } else {
14641 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
14642 $this->_admin_notices->remove_sticky( 'affiliate_program' );
14643 }
14644
14645 $affiliate_application_data = array(
14646 'status' => 'pending',
14647 'stats_description' => $affiliate['stats_description'],
14648 'promotion_method_description' => $affiliate['promotion_method_description'],
14649 );
14650
14651 if ( ! empty( $affiliate['promotion_methods'] ) ) {
14652 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
14653 }
14654
14655 if ( ! empty( $affiliate['domain'] ) ) {
14656 $affiliate_application_data['domain'] = $affiliate['domain'];
14657 }
14658
14659 if ( ! empty( $affiliate['additional_domains'] ) ) {
14660 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
14661 }
14662
14663 $this->_storage->affiliate_application_data = $affiliate_application_data;
14664 }
14665
14666 // Purge cached affiliate.
14667 $api->purge_cache( 'affiliate.json' );
14668
14669 self::shoot_ajax_success( $result );
14670 }
14671
14672 /**
14673 * @author Leo Fajardo (@leorw)
14674 * @since 1.2.3
14675 *
14676 * @return array|null
14677 */
14678 function get_affiliate_application_data() {
14679 if ( empty( $this->_storage->affiliate_application_data ) ) {
14680 return null;
14681 }
14682
14683 return $this->_storage->affiliate_application_data;
14684 }
14685
14686 #endregion Affiliation ------------------------------------------------------------
14687
14688 #----------------------------------------------------------------------------------
14689 #region URL Generators
14690 #----------------------------------------------------------------------------------
14691
14692 /**
14693 * Alias to pricing_url().
14694 *
14695 * @author Vova Feldman (@svovaf)
14696 * @since 1.0.2
14697 *
14698 * @uses pricing_url()
14699 *
14700 * @param string $period Billing cycle
14701 * @param bool $is_trial
14702 *
14703 * @return string
14704 */
14705 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14706 return $this->pricing_url( $period, $is_trial );
14707 }
14708
14709 /**
14710 * @author Vova Feldman (@svovaf)
14711 * @since 1.0.9
14712 *
14713 * @uses get_upgrade_url()
14714 *
14715 * @return string
14716 */
14717 function get_trial_url() {
14718 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
14719 }
14720
14721 /**
14722 * @author Leo Fajardo (@leorw)
14723 * @since 2.1.4
14724 *
14725 * @param string $new_version
14726 *
14727 * @return string
14728 */
14729 function version_upgrade_checkout_link( $new_version ) {
14730 if ( ! is_object( $this->_license ) ) {
14731 $url = $this->pricing_url();
14732
14733 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
14734 } else {
14735 $subscription = $this->_get_subscription( $this->_license->id );
14736
14737 $url = $this->checkout_url(
14738 is_object( $subscription ) ?
14739 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
14740 WP_FS__PERIOD_LIFETIME,
14741 false,
14742 array( 'licenses' => $this->_license->quota )
14743 );
14744
14745 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
14746 }
14747
14748 return sprintf(
14749 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
14750 sprintf(
14751 '<a href="%s">%s</a>',
14752 $this->apply_filters( 'update_notice_checkout_url', $url ),
14753 $purchase_license_text
14754 ),
14755 $new_version
14756 );
14757 }
14758
14759 /**
14760 * Plugin's pricing URL.
14761 *
14762 * @author Vova Feldman (@svovaf)
14763 * @since 1.0.4
14764 *
14765 * @param string $billing_cycle Billing cycle
14766 *
14767 * @param bool $is_trial
14768 *
14769 * @return string
14770 */
14771 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
14772 $this->_logger->entrance();
14773
14774 $params = array(
14775 'billing_cycle' => $billing_cycle
14776 );
14777
14778 if ( $is_trial ) {
14779 $params['trial'] = 'true';
14780 }
14781
14782 $url = $this->is_addon() ?
14783 $this->_parent->addon_url( $this->_slug ) :
14784 $this->_get_admin_page_url( 'pricing', $params );
14785
14786 return $this->get_pricing_url_with_filter( $url );
14787 }
14788
14789 /**
14790 * Retrieves the filtered pricing URL.
14791 *
14792 * @author Leo Fajardo (@leorw)
14793 * @since 2.7.4
14794 *
14795 * @param string $url
14796 *
14797 * @return string
14798 */
14799 private function get_pricing_url_with_filter( $url ) {
14800 return $this->apply_filters( 'pricing_url', $url );
14801 }
14802
14803 /**
14804 * Checkout page URL.
14805 *
14806 * @author Vova Feldman (@svovaf)
14807 * @since 1.0.6
14808 *
14809 * @param string $billing_cycle Billing cycle
14810 * @param bool $is_trial
14811 * @param array $extra (optional) Extra parameters, override other query params.
14812 * @param bool|null $network
14813 *
14814 * @return string
14815 */
14816 function checkout_url(
14817 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14818 $is_trial = false,
14819 $extra = array(),
14820 $network = null
14821 ) {
14822 $this->_logger->entrance();
14823
14824 $params = array(
14825 'checkout' => 'true',
14826 'billing_cycle' => $billing_cycle,
14827 );
14828
14829 if ( $is_trial ) {
14830 $params['trial'] = 'true';
14831 }
14832
14833 /**
14834 * Params in extra override other params.
14835 */
14836 $params = array_merge( $params, $extra );
14837
14838 return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) );
14839 }
14840
14841 /**
14842 * Add-on checkout URL.
14843 *
14844 * @author Vova Feldman (@svovaf)
14845 * @since 1.1.7
14846 *
14847 * @param number $addon_id
14848 * @param number $pricing_id
14849 * @param string $billing_cycle
14850 * @param bool $is_trial
14851 * @param bool|null $network
14852 *
14853 * @return string
14854 */
14855 function addon_checkout_url(
14856 $addon_id,
14857 $pricing_id,
14858 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
14859 $is_trial = false,
14860 $network = null
14861 ) {
14862 return $this->checkout_url( $billing_cycle, $is_trial, array(
14863 'plugin_id' => $addon_id,
14864 'pricing_id' => $pricing_id,
14865 ), $network );
14866 }
14867
14868 #endregion
14869
14870 #endregion ------------------------------------------------------------------
14871
14872 /**
14873 * Check if plugin has any add-ons.
14874 *
14875 * @author Vova Feldman (@svovaf)
14876 * @since 1.0.5
14877 *
14878 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
14879 *
14880 * @return bool
14881 */
14882 function has_addons() {
14883 $this->_logger->entrance();
14884
14885 return $this->_has_addons;
14886 }
14887
14888 /**
14889 * Check if plugin can work in anonymous mode.
14890 *
14891 * @author Vova Feldman (@svovaf)
14892 * @since 1.0.9
14893 *
14894 * @return bool
14895 *
14896 * @deprecated Please use is_enable_anonymous() instead.
14897 */
14898 function enable_anonymous() {
14899 return $this->_enable_anonymous;
14900 }
14901
14902 /**
14903 * Check if plugin can work in anonymous mode.
14904 *
14905 * @author Vova Feldman (@svovaf)
14906 * @since 1.1.9
14907 *
14908 * @return bool
14909 */
14910 function is_enable_anonymous() {
14911 return $this->_enable_anonymous;
14912 }
14913
14914 /**
14915 * Check if plugin is premium only (no free plans).
14916 *
14917 * @author Vova Feldman (@svovaf)
14918 * @since 1.1.9
14919 *
14920 * @return bool
14921 */
14922 function is_only_premium() {
14923 return $this->_is_premium_only;
14924 }
14925
14926 /**
14927 * Checks if the plugin's type is "plugin". The other type is "theme".
14928 *
14929 * @author Leo Fajardo (@leorw)
14930 * @since 1.2.2
14931 *
14932 * @return bool
14933 */
14934 function is_plugin() {
14935 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
14936 }
14937
14938 /**
14939 * @author Leo Fajardo (@leorw)
14940 * @since 1.2.2
14941 *
14942 * @return string
14943 */
14944 function get_module_type() {
14945 if ( ! isset( $this->_module_type ) ) {
14946 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
14947 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
14948 }
14949
14950 return $this->_module_type;
14951 }
14952
14953 /**
14954 * @author Leo Fajardo (@leorw)
14955 * @since 1.2.2
14956 *
14957 * @return string
14958 */
14959 function get_plugin_main_file_path() {
14960 return $this->_plugin_main_file_path;
14961 }
14962
14963 /**
14964 * Check if module has a premium code version.
14965 *
14966 * Serviceware module might be freemium without any
14967 * premium code version, where the paid features
14968 * are all part of the service.
14969 *
14970 * @author Vova Feldman (@svovaf)
14971 * @since 1.2.1.6
14972 *
14973 * @return bool
14974 */
14975 function has_premium_version() {
14976 return $this->_has_premium_version;
14977 }
14978
14979 /**
14980 * Check if feature supported with current site's plan.
14981 *
14982 * @author Vova Feldman (@svovaf)
14983 * @since 1.0.1
14984 *
14985 * @todo IMPLEMENT
14986 *
14987 * @param number $feature_id
14988 *
14989 * @throws Exception
14990 */
14991 function is_feature_supported( $feature_id ) {
14992 throw new Exception( 'not implemented' );
14993 }
14994
14995 /**
14996 * @author Vova Feldman (@svovaf)
14997 * @since 1.0.1
14998 *
14999 * @return bool Is running in SSL/HTTPS
15000 */
15001 function is_ssl() {
15002 return WP_FS__IS_HTTPS;
15003 }
15004
15005 /**
15006 * @author Vova Feldman (@svovaf)
15007 * @since 1.0.9
15008 *
15009 * @return bool Is running in AJAX call.
15010 *
15011 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15012 */
15013 static function is_ajax() {
15014 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15015 }
15016
15017 /**
15018 * Check if it's an AJAX call targeted for the current module.
15019 *
15020 * @author Vova Feldman (@svovaf)
15021 * @since 1.2.0
15022 *
15023 * @param array|string $actions Collection of AJAX actions.
15024 *
15025 * @return bool
15026 */
15027 function is_ajax_action( $actions ) {
15028 // Verify it's an ajax call.
15029 if ( ! self::is_ajax() ) {
15030 return false;
15031 }
15032
15033 // Verify the call is relevant for the plugin.
15034 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15035 return false;
15036 }
15037
15038 // Verify it's one of the specified actions.
15039 if ( is_string( $actions ) ) {
15040 $actions = explode( ',', $actions );
15041 }
15042
15043 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15044 $ajax_action = fs_request_get( 'action' );
15045
15046 foreach ( $actions as $action ) {
15047 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15048 return true;
15049 }
15050 }
15051 }
15052
15053 return false;
15054 }
15055
15056 /**
15057 * Check if it's an AJAX call targeted for current request.
15058 *
15059 * @author Vova Feldman (@svovaf)
15060 * @since 1.2.0
15061 *
15062 * @param array|string $actions Collection of AJAX actions.
15063 * @param number|null $module_id
15064 *
15065 * @return bool
15066 */
15067 static function is_ajax_action_static( $actions, $module_id = null ) {
15068 // Verify it's an ajax call.
15069 if ( ! self::is_ajax() ) {
15070 return false;
15071 }
15072
15073
15074 if ( ! empty( $module_id ) ) {
15075 // Verify the call is relevant for the plugin.
15076 if ( $module_id != fs_request_get( 'module_id' ) ) {
15077 return false;
15078 }
15079 }
15080
15081 // Verify it's one of the specified actions.
15082 if ( is_string( $actions ) ) {
15083 $actions = explode( ',', $actions );
15084 }
15085
15086 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15087 $ajax_action = fs_request_get( 'action' );
15088
15089 foreach ( $actions as $action ) {
15090 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15091 return true;
15092 }
15093 }
15094 }
15095
15096 return false;
15097 }
15098
15099 /**
15100 * @author Vova Feldman (@svovaf)
15101 * @since 1.1.7
15102 *
15103 * @return bool
15104 */
15105 static function is_cron() {
15106 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15107 }
15108
15109 /**
15110 * @author Leo Fajardo (@leorw)
15111 * @since 2.5.0
15112 *
15113 * @return bool
15114 */
15115 static function is_admin_post() {
15116 return ( 'admin-post.php' === self::get_current_page() );
15117 }
15118
15119 /**
15120 * Check if a real user is visiting the admin dashboard.
15121 *
15122 * @author Vova Feldman (@svovaf)
15123 * @since 1.1.7
15124 *
15125 * @return bool
15126 */
15127 function is_user_in_admin() {
15128 return (
15129 is_admin() &&
15130 ! self::is_ajax() &&
15131 ! self::is_cron() &&
15132 ! self::is_admin_post()
15133 );
15134 }
15135
15136 /**
15137 * Check if a real user is in the customizer view.
15138 *
15139 * @author Vova Feldman (@svovaf)
15140 * @since 1.2.2.7
15141 *
15142 * @return bool
15143 */
15144 static function is_customizer() {
15145 return is_customize_preview();
15146 }
15147
15148 /**
15149 * Check if running in HTTPS and if site's plan matching the specified plan.
15150 *
15151 * @param string $plan
15152 * @param bool $exact
15153 *
15154 * @return bool
15155 */
15156 function is_ssl_and_plan( $plan, $exact = false ) {
15157 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15158 }
15159
15160 /**
15161 * Construct plugin's settings page URL.
15162 *
15163 * @author Vova Feldman (@svovaf)
15164 * @since 1.0.4
15165 *
15166 * @param string $page
15167 * @param array $params
15168 * @param bool|null $network
15169 *
15170 * @return string
15171 */
15172 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15173 if ( is_null( $network ) ) {
15174 $network = (
15175 $this->_is_network_active &&
15176 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15177 );
15178 }
15179
15180 if ( 0 < count( $params ) ) {
15181 foreach ( $params as $k => $v ) {
15182 $params[ $k ] = urlencode( $v );
15183 }
15184 }
15185
15186 $page_param = $this->_menu->get_slug( $page );
15187
15188 if ( empty( $page ) &&
15189 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15190 $this->show_opt_in_on_themes_page()
15191 ) {
15192 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15193
15194 return add_query_arg(
15195 $params,
15196 $this->admin_url( 'themes.php', 'admin', $network )
15197 );
15198 }
15199
15200 if ( ! $this->has_settings_menu() ) {
15201 if ( ! empty( $page ) ) {
15202 // Module doesn't have a setting page, but since the request is for
15203 // a specific Freemius page, use the admin.php path.
15204 return add_query_arg( array_merge( $params, array(
15205 'page' => $page_param,
15206 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15207 } else {
15208 if ( $this->is_activation_mode() ) {
15209 /**
15210 * @author Vova Feldman
15211 * @since 1.2.1.6
15212 *
15213 * If plugin doesn't have a settings page, create one for the opt-in screen.
15214 */
15215 return add_query_arg( array_merge( $params, array(
15216 'page' => $this->_slug,
15217 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15218 } else {
15219 // Plugin without a settings page.
15220 return add_query_arg(
15221 $params,
15222 $this->admin_url( 'plugins.php', 'admin', $network )
15223 );
15224 }
15225 }
15226 }
15227
15228 // Module has a submenu settings page.
15229 if ( ! $this->_menu->is_top_level() ) {
15230 $parent_slug = $this->_menu->get_parent_slug();
15231 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15232 $parent_slug :
15233 'admin.php';
15234
15235 return add_query_arg( array_merge( $params, array(
15236 'page' => $page_param,
15237 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15238 }
15239
15240 // Module has a top level CPT settings page.
15241 if ( $this->_menu->is_cpt() ) {
15242 if ( empty( $page ) && $this->is_activation_mode() ) {
15243 return add_query_arg( array_merge( $params, array(
15244 'page' => $page_param
15245 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15246 } else {
15247 if ( ! empty( $page ) ) {
15248 $params['page'] = $page_param;
15249 }
15250
15251 return add_query_arg(
15252 $params,
15253 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15254 );
15255 }
15256 }
15257
15258 // Module has a custom top level settings page.
15259 return add_query_arg( array_merge( $params, array(
15260 'page' => $page_param,
15261 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15262 }
15263
15264 #--------------------------------------------------------------------------------
15265 #region Multisite
15266 #--------------------------------------------------------------------------------
15267
15268 /**
15269 * @author Leo Fajardo (@leorw)
15270 * @since 2.0.0
15271 *
15272 * @return bool
15273 */
15274 function is_network_active() {
15275 return $this->_is_network_active;
15276 }
15277
15278 /**
15279 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15280 *
15281 * @author Leo Fajardo (@leorw)
15282 * @since 2.0.0
15283 *
15284 * @param bool|int[] $all_or_blog_ids
15285 */
15286 private function delegate_connection( $all_or_blog_ids = true ) {
15287 $this->_logger->entrance();
15288
15289 $this->_admin_notices->remove_sticky( 'connect_account' );
15290
15291 if ( true === $all_or_blog_ids ) {
15292 // All sites delegation.
15293 $this->_storage->store( 'is_delegated_connection', true, true );
15294 } else {
15295 // Specified sites delegation.
15296 foreach ( $all_or_blog_ids as $blog_id ) {
15297 $this->delegate_site_connection( $blog_id );
15298 }
15299 }
15300
15301 $this->network_upgrade_mode_completed();
15302 }
15303
15304 /**
15305 * Delegate specific network site conncetion to the site admin.
15306 *
15307 * @author Vova Feldman (@svovaf)
15308 * @since 2.0.0
15309 *
15310 * @param int $blog_id
15311 */
15312 private function delegate_site_connection( $blog_id ) {
15313 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15314 }
15315
15316 /**
15317 * Check if super-admin delegated the connection of ALL sites to the site admins.
15318 *
15319 * @author Vova Feldman (@svovaf)
15320 * @since 2.0.0
15321 *
15322 * @return bool
15323 */
15324 function is_network_delegated_connection() {
15325 if ( ! $this->_is_network_active ) {
15326 return false;
15327 }
15328
15329 return $this->_storage->get( 'is_delegated_connection', false, true );
15330 }
15331
15332 /**
15333 * @author Leo Fajardo (@leorw)
15334 * @since 2.0.0
15335 *
15336 * @param int $blog_id
15337 *
15338 * @return bool
15339 */
15340 function is_site_delegated_connection( $blog_id = 0 ) {
15341 if ( ! $this->_is_network_active ) {
15342 return false;
15343 }
15344
15345 if ( 0 == $blog_id ) {
15346 $blog_id = get_current_blog_id();
15347 }
15348
15349 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15350 }
15351
15352 /**
15353 * Check if delegated the connection. When running within the network admin,
15354 * and haven't specified the blog ID, checks if network level delegated. If running
15355 * within a site admin or specified a blog ID, check if delegated the connection for
15356 * the current context site.
15357 *
15358 * If executed outside the the admin, check if delegated the connection
15359 * for the current context site OR the whole network.
15360 *
15361 * @author Vova Feldman (@svovaf)
15362 * @since 2.0.0
15363 *
15364 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15365 *
15366 * @return bool
15367 */
15368 function is_delegated_connection( $blog_id = 0 ) {
15369 if ( ! $this->_is_network_active ) {
15370 return false;
15371 }
15372
15373 if ( fs_is_network_admin() && 0 == $blog_id ) {
15374 return $this->is_network_delegated_connection();
15375 }
15376
15377 return (
15378 $this->is_network_delegated_connection() ||
15379 $this->is_site_delegated_connection( $blog_id )
15380 );
15381 }
15382
15383 /**
15384 * Check if the current module is active for the site.
15385 *
15386 * @author Vova Feldman (@svovaf)
15387 * @since 2.0.0
15388 *
15389 * @param int $blog_id
15390 *
15391 * @return bool
15392 */
15393 function is_active_for_site( $blog_id ) {
15394 if ( ! is_multisite() ) {
15395 // Not a multisite and this code is executed, means that the plugin is active.
15396 return true;
15397 }
15398
15399 if ( $this->is_theme() ) {
15400 // All themes are site level activated.
15401 return true;
15402 }
15403
15404 if ( $this->_is_network_active ) {
15405 // Plugin was network activated so it's active.
15406 return true;
15407 }
15408
15409 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15410 }
15411
15412 /**
15413 * @todo Implement pagination when accessing the subsites collection.
15414 *
15415 * @author Leo Fajardo (@leorw)
15416 * @since 2.0.0
15417 *
15418 * @param int $limit Default to 1,000
15419 * @param int $offset Default to 0
15420 *
15421 * @return array Active & public sites collection.
15422 */
15423 static function get_sites( $limit = 1000, $offset = 0 ) {
15424 if ( ! is_multisite() ) {
15425 return array();
15426 }
15427
15428 /**
15429 * For consistency with get_blog_list() which only return active public sites.
15430 *
15431 * @author Vova Feldman (@svovaf)
15432 */
15433 $args = array(
15434 /**
15435 * Commented out in order to handle the migration of site options whether the site is public or not.
15436 *
15437 * @author Leo Fajardo (@leorw)
15438 * @since 2.2.1
15439 */
15440 // 'public' => 1,
15441 'archived' => 0,
15442 'mature' => 0,
15443 'spam' => 0,
15444 'deleted' => 0,
15445 'number' => $limit,
15446 'offset' => $offset,
15447 );
15448
15449 return get_sites( $args );
15450 }
15451
15452 /**
15453 * Checks if a given blog is active.
15454 *
15455 * @author Vova Feldman (@svovaf)
15456 * @since 2.0.0
15457 *
15458 * @param $blog_id
15459 *
15460 * @return bool
15461 */
15462 private static function is_site_active( $blog_id ) {
15463 global $wpdb;
15464
15465 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15466
15467 if ( ! is_object( $blog_info ) ) {
15468 return false;
15469 }
15470
15471 return (
15472 true == $blog_info->public &&
15473 false == $blog_info->archived &&
15474 false == $blog_info->mature &&
15475 false == $blog_info->spam &&
15476 false == $blog_info->deleted
15477 );
15478 }
15479
15480 /**
15481 * Get a mapping between the site addresses to their blog IDs.
15482 *
15483 * @author Vova Feldman (@svovaf)
15484 * @since 2.0.0
15485 *
15486 * @return array {
15487 * @key string Site address without protocol with a trailing slash.
15488 * @value int Site's blog ID.
15489 * }
15490 */
15491 private function get_address_to_blog_map() {
15492 $sites = self::get_sites();
15493
15494 // Map site addresses to their blog IDs.
15495 $address_to_blog_map = array();
15496 foreach ( $sites as $site ) {
15497 $blog_id = self::get_site_blog_id( $site );
15498 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15499 $address_to_blog_map[ $address ] = $blog_id;
15500 }
15501
15502 return $address_to_blog_map;
15503 }
15504
15505 /**
15506 * Get a mapping between the site addresses to their blog IDs.
15507 *
15508 * @author Vova Feldman (@svovaf)
15509 * @since 2.0.0
15510 *
15511 * @return array {
15512 * @key int Site's blog ID.
15513 * @value FS_Site Associated install.
15514 * }
15515 */
15516 function get_blog_install_map() {
15517 $sites = self::get_sites();
15518
15519 // Map site blog ID to its install.
15520 $install_map = array();
15521
15522 foreach ( $sites as $site ) {
15523 $blog_id = self::get_site_blog_id( $site );
15524 $install = $this->get_install_by_blog_id( $blog_id );
15525
15526 if ( is_object( $install ) ) {
15527 $install_map[ $blog_id ] = $install;
15528 }
15529 }
15530
15531 return $install_map;
15532 }
15533
15534 /**
15535 * @author Vova Feldman (@svovaf)
15536 * @since 2.5.1
15537 *
15538 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15539 *
15540 * @return int[]
15541 */
15542 private function get_blog_ids( $is_delegated = null ) {
15543 $blog_ids = array();
15544
15545 $sites = self::get_sites();
15546 foreach ( $sites as $site ) {
15547 $blog_id = self::get_site_blog_id( $site );
15548
15549 if (
15550 is_null( $is_delegated ) ||
15551 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15552 ) {
15553 $blog_ids[] = $blog_id;
15554 }
15555 }
15556
15557 return $blog_ids;
15558 }
15559
15560 /**
15561 * @author Vova Feldman (@svovaf)
15562 * @since 2.5.1
15563 *
15564 * @return int[]
15565 */
15566 private function get_non_delegated_blog_ids() {
15567 return $this->get_blog_ids( false );
15568 }
15569
15570 /**
15571 * Gets a map of module IDs that the given user has opted-in to.
15572 *
15573 * @author Leo Fajardo (@leorw)
15574 * @since 2.1.0
15575 *
15576 * @param number $fs_user_id
15577 *
15578 * @return array {
15579 * @key number $plugin_id
15580 * @value bool Always true.
15581 * }
15582 */
15583 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15584 self::$_static_logger->entrance();
15585
15586 if ( ! is_multisite() ) {
15587 $installs = array_merge(
15588 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15589 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15590 );
15591 } else {
15592 $sites = self::get_sites();
15593
15594 $installs = array();
15595 foreach ( $sites as $site ) {
15596 $blog_id = self::get_site_blog_id( $site );
15597
15598 $installs = array_merge(
15599 $installs,
15600 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15601 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15602 );
15603 }
15604 }
15605
15606 $module_ids_map = array();
15607 foreach ( $installs as $install ) {
15608 if ( is_object( $install ) &&
15609 FS_Site::is_valid_id( $install->id ) &&
15610 FS_User::is_valid_id( $install->user_id ) &&
15611 ( $install->user_id == $fs_user_id )
15612 ) {
15613 $module_ids_map[ $install->plugin_id ] = true;
15614 }
15615 }
15616
15617 return $module_ids_map;
15618 }
15619
15620 /**
15621 * @author Leo Fajardo (@leorw)
15622 *
15623 * @return null|array {
15624 * 'install' => FS_Site Module's install,
15625 * 'blog_id' => string The associated blog ID.
15626 * }
15627 */
15628 function find_first_install() {
15629 $sites = self::get_sites();
15630
15631 foreach ( $sites as $site ) {
15632 $blog_id = self::get_site_blog_id( $site );
15633 $install = $this->get_install_by_blog_id( $blog_id );
15634
15635 if ( is_object( $install ) ) {
15636 return array(
15637 'install' => $install,
15638 'blog_id' => $blog_id
15639 );
15640 }
15641 }
15642
15643 return null;
15644 }
15645
15646 /**
15647 * Switches the Freemius site level context to a specified blog.
15648 *
15649 * @author Vova Feldman (@svovaf)
15650 * @since 2.0.0
15651 *
15652 * @param int $blog_id
15653 * @param FS_Site $install
15654 * @param bool $flush
15655 *
15656 * @return bool Since 2.3.1 returns if a switch was made.
15657 */
15658 function switch_to_blog( $blog_id, $install = null, $flush = false ) {
15659 if ( ! is_numeric( $blog_id ) ) {
15660 return false;
15661 }
15662
15663 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
15664 return false;
15665 }
15666
15667 switch_to_blog( $blog_id );
15668 $this->_context_is_network_or_blog_id = $blog_id;
15669
15670 self::$_accounts->set_site_blog_context( $blog_id );
15671 $this->_storage->set_site_blog_context( $blog_id );
15672 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
15673
15674 $this->_site = is_object( $install ) ?
15675 $install :
15676 $this->get_install_by_blog_id( $blog_id );
15677
15678 $this->_user = false;
15679 $this->_licenses = false;
15680 $this->_license = null;
15681 $this->is_whitelabeled = null;
15682
15683 if ( is_object( $this->_site ) ) {
15684 // Try to fetch user from install.
15685 $this->_user = self::_get_user_by_id( $this->_site->user_id );
15686
15687 if ( ! is_object( $this->_user ) &&
15688 FS_User::is_valid_id( $this->_storage->prev_user_id )
15689 ) {
15690 // Try to fetch previously saved user.
15691 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
15692
15693 if ( ! is_object( $this->_user ) ) {
15694 // Fallback to network's user.
15695 $this->_user = $this->get_network_user();
15696 }
15697 }
15698
15699 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
15700
15701 if ( ! empty( $all_plugin_licenses ) ) {
15702 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
15703 $this->_license = null;
15704 } else {
15705 $license_found = false;
15706 foreach ( $all_plugin_licenses as $license ) {
15707 if ( $license->id == $this->_site->license_id ) {
15708 // License found.
15709 $this->_license = $license;
15710 $license_found = true;
15711 break;
15712 }
15713 }
15714
15715 if ( $license_found ) {
15716 $this->link_license_2_user( $this->_license->id, $this->_user->id );
15717 }
15718 }
15719
15720 $this->_licenses = $this->get_user_licenses( $this->_user->id );
15721 }
15722 }
15723
15724 unset( $this->_site_api );
15725 unset( $this->_user_api );
15726
15727 return true;
15728 }
15729
15730 /**
15731 * Restore the blog context to the blog that originally loaded the module.
15732 *
15733 * @author Vova Feldman (@svovaf)
15734 * @since 2.0.0
15735 */
15736 function restore_current_blog() {
15737 $this->switch_to_blog( $this->_blog_id );
15738 }
15739
15740 /**
15741 * @author Vova Feldman (@svovaf)
15742 * @since 2.0.0
15743 *
15744 * @param array|WP_Site $site
15745 *
15746 * @return int
15747 */
15748 static function get_site_blog_id( &$site ) {
15749 return ( $site instanceof WP_Site ) ?
15750 $site->blog_id :
15751 ( is_object( $site ) && isset( $site->userblog_id ) ?
15752 $site->userblog_id :
15753 $site['blog_id'] );
15754 }
15755
15756 /**
15757 * @author Vova Feldman (@svovaf)
15758 * @since 2.5.1
15759 *
15760 * @param WP_Site[]|array[] $sites
15761 *
15762 * @return int[]
15763 */
15764 static function get_sites_blog_ids( $sites ) {
15765 $blog_ids = array();
15766 foreach ( $sites as $site ) {
15767 $blog_ids[] = self::get_site_blog_id( $site );
15768 }
15769
15770 return $blog_ids;
15771 }
15772
15773 /**
15774 * @author Leo Fajardo (@leorw)
15775 * @since 2.0.0
15776 *
15777 * @param array|WP_Site|null $site
15778 * @param bool $load_registration Since 2.5.1 When set to `true` the method will attempt to return the subsite's registration date, regardless of the `$site` type and value. In most calls, the registration date will be returned anyway, even when the value is `false`. This param is purely for performance optimization.
15779 *
15780 * @return array
15781 */
15782 function get_site_info( $site = null, $load_registration = false ) {
15783 $this->_logger->entrance();
15784
15785 $fs_hook_snapshot = new FS_Hook_Snapshot();
15786 // Remove all filters from `switch_blog`.
15787 $fs_hook_snapshot->remove( 'switch_blog' );
15788
15789 $switched = false;
15790
15791 $registration_date = null;
15792
15793 if ( is_null( $site ) ) {
15794 $url = self::get_unfiltered_site_url();
15795 $name = get_bloginfo( 'name' );
15796 $blog_id = null;
15797 } else {
15798 $blog_id = self::get_site_blog_id( $site );
15799
15800 if ( get_current_blog_id() != $blog_id ) {
15801 switch_to_blog( $blog_id );
15802 $switched = true;
15803 }
15804
15805 if ( $site instanceof WP_Site ) {
15806 $url = $site->siteurl;
15807 $name = $site->blogname;
15808 $registration_date = $site->registered;
15809 } else {
15810 $url = self::get_unfiltered_site_url( $blog_id );
15811 $name = get_bloginfo( 'name' );
15812 }
15813 }
15814
15815 if ( empty( $registration_date ) && $load_registration ) {
15816 $blog_details = get_blog_details( $blog_id, false );
15817
15818 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
15819 $registration_date = $blog_details->registered;
15820 }
15821 }
15822
15823 $info = array(
15824 'uid' => $this->get_anonymous_id( $blog_id ),
15825 'url' => $url,
15826 );
15827
15828 // Add these diagnostic information only if user allowed to track.
15829 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
15830 $info = array_merge( $info, array(
15831 'title' => $name,
15832 'language' => self::get_sanitized_language(),
15833 ) );
15834 }
15835
15836 if ( is_numeric( $blog_id ) ) {
15837 $info['blog_id'] = $blog_id;
15838 }
15839
15840 if ( ! empty( $registration_date ) ) {
15841 $info[ 'registration_date' ] = $registration_date;
15842 }
15843
15844 if ( $switched ) {
15845 restore_current_blog();
15846 }
15847
15848 // Add the filters back to `switch_blog`.
15849 $fs_hook_snapshot->restore( 'switch_blog' );
15850
15851 return $info;
15852 }
15853
15854 /**
15855 * Load the module's install based on the blog ID.
15856 *
15857 * @author Vova Feldman (@svovaf)
15858 * @since 2.0.0
15859 *
15860 * @param int|null $blog_id
15861 *
15862 * @return FS_Site
15863 */
15864 function get_install_by_blog_id( $blog_id = null ) {
15865 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15866 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15867
15868 if ( is_object( $install ) &&
15869 is_numeric( $install->id ) &&
15870 is_numeric( $install->user_id ) &&
15871 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15872 ) {
15873 // Load site.
15874 $install = clone $install;
15875 }
15876
15877 return $install;
15878 }
15879
15880 /**
15881 * Check if module is installed on a specified site.
15882 *
15883 * @author Vova Feldman (@svovaf)
15884 * @since 2.0.0
15885 *
15886 * @param int|null $blog_id
15887 *
15888 * @return bool
15889 */
15890 function is_installed_on_site( $blog_id = null ) {
15891 $installs = self::get_all_sites( $this->_module_type, $blog_id );
15892 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
15893
15894 return (
15895 is_object( $install ) &&
15896 is_numeric( $install->id ) &&
15897 is_numeric( $install->user_id ) &&
15898 FS_Plugin_Plan::is_valid_id( $install->plan_id )
15899 );
15900 }
15901
15902 /**
15903 * Check if super-admin connected at least one site via the network opt-in.
15904 *
15905 * @author Vova Feldman (@svovaf)
15906 * @since 2.0.0
15907 *
15908 * @return bool
15909 */
15910 function is_network_registered() {
15911 if ( ! $this->_is_network_active ) {
15912 return false;
15913 }
15914
15915 return FS_User::is_valid_id( $this->_storage->network_user_id );
15916 }
15917
15918 /**
15919 * Returns the main user associated with the network.
15920 *
15921 * @author Vova Feldman (@svovaf)
15922 * @since 2.0.0
15923 *
15924 * @return FS_User
15925 */
15926 function get_network_user() {
15927 if ( ! $this->_is_network_active ) {
15928 return null;
15929 }
15930
15931 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
15932 self::_get_user_by_id( $this->_storage->network_user_id ) :
15933 null;
15934 }
15935
15936 /**
15937 * Returns the current context user or the network's main user.
15938 *
15939 * @author Vova Feldman (@svovaf)
15940 * @since 2.0.0
15941 *
15942 * @return FS_User
15943 */
15944 function get_current_or_network_user() {
15945 return ( $this->_user instanceof FS_User ) ?
15946 $this->_user :
15947 $this->get_network_user();
15948 }
15949
15950 /**
15951 * Returns the main install associated with the network.
15952 *
15953 * @author Vova Feldman (@svovaf)
15954 * @since 2.0.0
15955 *
15956 * @return FS_Site
15957 */
15958 function get_network_install() {
15959 if ( ! $this->_is_network_active ) {
15960 return null;
15961 }
15962
15963 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15964 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
15965 null;
15966 }
15967
15968 /**
15969 * Returns the blog ID that is associated with the main install.
15970 *
15971 * @author Leo Fajardo (@leorw)
15972 * @since 2.0.0
15973 *
15974 * @return int|null
15975 */
15976 function get_network_install_blog_id() {
15977 if ( ! $this->_is_network_active ) {
15978 return null;
15979 }
15980
15981 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
15982 $this->_storage->network_install_blog_id :
15983 null;
15984 }
15985
15986 /**
15987 * Returns the current context install or the network's main install.
15988 *
15989 * @author Vova Feldman (@svovaf)
15990 * @since 2.0.0
15991 *
15992 * @return FS_Site
15993 */
15994 function get_current_or_network_install() {
15995 return ( $this->_site instanceof FS_Site ) ?
15996 $this->_site :
15997 $this->get_network_install();
15998 }
15999
16000 /**
16001 * Check if executing a site level action from the network level admin.
16002 *
16003 * @author Vova Feldman (@svovaf)
16004 * @since 2.0.0
16005 *
16006 * @return false|int If yes, return the requested blog ID.
16007 */
16008 private function is_network_level_site_specific_action() {
16009 if ( ! $this->_is_network_active ) {
16010 return false;
16011 }
16012
16013 if ( ! fs_is_network_admin() ) {
16014 return false;
16015 }
16016
16017 $blog_id = fs_request_get( 'blog_id', '' );
16018
16019 return is_numeric( $blog_id ) ? $blog_id : false;
16020 }
16021
16022 /**
16023 * Check if executing an action from the network level admin.
16024 *
16025 * @author Vova Feldman (@svovaf)
16026 * @since 2.0.0
16027 *
16028 * @return bool
16029 */
16030 private function is_network_level_action() {
16031 return ( $this->_is_network_active && fs_is_network_admin() );
16032 }
16033
16034 /**
16035 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16036 * 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.
16037 *
16038 * @author Vova Feldman (@svovaf)
16039 * @since 2.0.0
16040 *
16041 * @param int $context_blog_id
16042 */
16043 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16044 $this->_logger->entrance();
16045
16046 if ( $this->_is_network_active ) {
16047 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16048 $installs_map = $this->get_blog_install_map();
16049
16050 foreach ( $installs_map as $blog_id => $install ) {
16051 /**
16052 * @var FS_Site $install
16053 */
16054 if ( $context_blog_id == $blog_id ) {
16055 continue;
16056 }
16057
16058 if ( $install->user_id != $this->_storage->network_user_id ) {
16059 continue;
16060 }
16061
16062 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16063 $this->_storage->network_install_blog_id = $blog_id;
16064 break;
16065 }
16066 }
16067 }
16068
16069 if ( ! $this->is_registered() ) {
16070 return;
16071 }
16072
16073 if ( $this->is_sync_cron_scheduled() &&
16074 $context_blog_id == $this->get_sync_cron_blog_id()
16075 ) {
16076 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16077 }
16078
16079 if ( $this->is_install_sync_scheduled() &&
16080 $context_blog_id == $this->get_install_sync_cron_blog_id()
16081 ) {
16082 $this->maybe_schedule_install_sync_cron( $context_blog_id );
16083 }
16084 }
16085
16086 /**
16087 * Executed after site deactivation, archive, or flag as spam.
16088 *
16089 * @author Vova Feldman (@svovaf)
16090 * @since 2.0.0
16091 *
16092 * @param int $context_blog_id
16093 */
16094 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16095 $this->_logger->entrance();
16096
16097 $install = $this->get_install_by_blog_id( $context_blog_id );
16098
16099 if ( ! is_object( $install ) ) {
16100 // Site not connected.
16101 return;
16102 }
16103
16104 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16105
16106 if ( ! $this->is_registered() ) {
16107 return;
16108 }
16109
16110 $current_blog_id = get_current_blog_id();
16111
16112 $this->switch_to_blog( $context_blog_id );
16113
16114 // Send deactivation event.
16115 $this->sync_install( array(
16116 'is_active' => false,
16117 ) );
16118
16119 $this->switch_to_blog( $current_blog_id );
16120 }
16121
16122 /**
16123 * Executed after site deletion.
16124 *
16125 * @author Vova Feldman (@svovaf)
16126 * @since 2.0.0
16127 *
16128 * @param int $context_blog_id
16129 * @param bool $drop True if site's database tables should be dropped. Default is false.
16130 */
16131 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16132 $this->_logger->entrance();
16133
16134 $install = $this->get_install_by_blog_id( $context_blog_id );
16135
16136 if ( ! is_object( $install ) ) {
16137 // Site not connected.
16138 return;
16139 }
16140
16141 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16142
16143 if ( ! $this->is_registered() ) {
16144 return;
16145 }
16146
16147 $current_blog_id = get_current_blog_id();
16148
16149 $this->switch_to_blog( $context_blog_id );
16150
16151 if ( $drop ) {
16152 // Delete install if dropping site DB.
16153 $this->delete_account_event();
16154 } else {
16155 // Send deactivation event.
16156 $this->sync_install( array(
16157 'is_active' => false,
16158 ) );
16159 }
16160
16161 $this->switch_to_blog( $current_blog_id );
16162 }
16163
16164 /**
16165 * Executed after site deletion, called from wp_delete_site
16166 *
16167 * @author Dario Curvino (@dudo)
16168 * @since 2.5.0
16169 *
16170 * @param WP_Site $old_site
16171 */
16172 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16173 $this->_logger->entrance();
16174
16175 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16176 }
16177
16178 /**
16179 * Executed after site re-activation.
16180 *
16181 * @author Vova Feldman (@svovaf)
16182 * @since 2.0.0
16183 *
16184 * @param int $context_blog_id
16185 */
16186 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16187 $this->_logger->entrance();
16188
16189 $install = $this->get_install_by_blog_id( $context_blog_id );
16190
16191 if ( ! is_object( $install ) ) {
16192 // Site not connected.
16193 return;
16194 }
16195
16196 if ( ! self::is_site_active( $context_blog_id ) ) {
16197 // Site not yet active (can be in spam mode, archived, deleted...).
16198 return;
16199 }
16200
16201 $current_blog_id = get_current_blog_id();
16202
16203 $this->switch_to_blog( $context_blog_id );
16204
16205 // Send re-activation event.
16206 $this->sync_install( array(
16207 'is_active' => true,
16208 ) );
16209
16210 $this->switch_to_blog( $current_blog_id );
16211 }
16212
16213 #endregion Multisite
16214
16215 /**
16216 * @author Leo Fajardo (@leorw)
16217 *
16218 * @param string $path
16219 * @param string $scheme
16220 * @param bool $network
16221 *
16222 * @return string
16223 */
16224 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16225 return ( $this->_is_network_active && $network ) ?
16226 network_admin_url( $path, $scheme ) :
16227 admin_url( $path, $scheme );
16228 }
16229
16230 /**
16231 * Check if currently in a specified admin page.
16232 *
16233 * @author Vova Feldman (@svovaf)
16234 * @since 1.2.2.7
16235 *
16236 * @param string $page
16237 *
16238 * @return bool
16239 */
16240 function is_admin_page( $page ) {
16241 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16242 }
16243
16244 /**
16245 * Check if currently in the product's main admin page.
16246 *
16247 * @author Vova Feldman (@svovaf)
16248 * @since 2.3.1
16249 *
16250 * @return bool
16251 */
16252 function is_main_admin_page() {
16253 return $this->is_admin_page( '' );
16254 }
16255
16256 /**
16257 * Get module's main admin setting page URL.
16258 *
16259 * @author Vova Feldman (@svovaf)
16260 * @since 1.2.2.7
16261 *
16262 * @return string
16263 */
16264 function main_menu_url() {
16265 return $this->_menu->main_menu_url();
16266 }
16267
16268 /**
16269 * Check if currently on the theme's setting page or
16270 * on any of the Freemius added pages (via tabs).
16271 *
16272 * @author Vova Feldman (@svovaf)
16273 * @since 1.2.2.7
16274 *
16275 * @return bool
16276 *
16277 * @deprecated Please use is_product_settings_page() instead;
16278 */
16279 function is_theme_settings_page() {
16280 return $this->is_product_settings_page();
16281 }
16282
16283 /**
16284 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16285 *
16286 * @author Vova Feldman (@svovaf)
16287 * @since 1.2.2.7
16288 *
16289 * @return bool
16290 */
16291 function is_product_settings_page() {
16292 $page = fs_request_get( 'page', '', 'get' );
16293 $menu_slug = $this->_menu->get_slug();
16294
16295 if ( $page === $menu_slug ) {
16296 return true;
16297 }
16298
16299 return fs_starts_with(
16300 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16301 $page,
16302 ( $menu_slug . '-' )
16303 );
16304 }
16305
16306 /**
16307 * Plugin's account page + sync license URL.
16308 *
16309 * @author Vova Feldman (@svovaf)
16310 * @since 1.1.9.1
16311 *
16312 * @param bool|number $plugin_id
16313 * @param bool $add_action_nonce
16314 * @param array $params
16315 *
16316 * @return string
16317 */
16318 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16319 if ( is_numeric( $plugin_id ) ) {
16320 $params['plugin_id'] = $plugin_id;
16321 }
16322
16323 return $this->get_account_url(
16324 $this->get_unique_affix() . '_sync_license',
16325 $params,
16326 $add_action_nonce
16327 );
16328 }
16329
16330 /**
16331 * Plugin's account URL.
16332 *
16333 * @author Vova Feldman (@svovaf)
16334 * @since 1.0.4
16335 *
16336 * @param bool|string $action
16337 * @param array $params
16338 *
16339 * @param bool $add_action_nonce
16340 *
16341 * @return string
16342 */
16343 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16344 if ( is_string( $action ) ) {
16345 $params['fs_action'] = $action;
16346 }
16347
16348 self::require_pluggable_essentials();
16349
16350 return ( $add_action_nonce && is_string( $action ) ) ?
16351 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16352 $this->_get_admin_page_url( 'account', $params );
16353 }
16354
16355 /**
16356 * @author Vova Feldman (@svovaf)
16357 * @since 1.2.0
16358 *
16359 * @param string $tab
16360 * @param bool $action
16361 * @param array $params
16362 * @param bool $add_action_nonce
16363 *
16364 * @return string
16365 *
16366 * @uses get_account_url()
16367 */
16368 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16369 $params['tab'] = $tab;
16370
16371 return $this->get_account_url( $action, $params, $add_action_nonce );
16372 }
16373
16374 /**
16375 * Plugin's account URL.
16376 *
16377 * @author Vova Feldman (@svovaf)
16378 * @since 1.0.4
16379 *
16380 * @param bool|string $topic
16381 * @param bool|string $message
16382 * @param bool|string $summary Since 2.5.1.
16383 *
16384 * @return string
16385 */
16386 function contact_url( $topic = false, $message = false, $summary = false ) {
16387 $params = array();
16388 if ( is_string( $topic ) ) {
16389 $params['topic'] = $topic;
16390 }
16391 if ( is_string( $message ) ) {
16392 $params['message'] = $message;
16393 }
16394
16395 if ( is_string( $summary ) ) {
16396 $params['summary'] = $summary;
16397 }
16398
16399 if ( $this->is_addon() ) {
16400 $params['addon_id'] = $this->get_id();
16401
16402 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16403 } else {
16404 return $this->_get_admin_page_url( 'contact', $params );
16405 }
16406 }
16407
16408 /**
16409 * Add-on direct info URL.
16410 *
16411 * @author Vova Feldman (@svovaf)
16412 * @since 1.1.0
16413 *
16414 * @param string $slug
16415 *
16416 * @return string
16417 */
16418 function addon_url( $slug ) {
16419 return $this->_get_admin_page_url( 'addons', array(
16420 'slug' => $slug
16421 ) );
16422 }
16423
16424 /**
16425 * Add-ons URL.
16426 *
16427 * @author Vova Feldman (@svovaf)
16428 * @since 2.4.5
16429 *
16430 * @return string
16431 */
16432 function get_addons_url() {
16433 return $this->_get_admin_page_url( 'addons' );
16434 }
16435
16436 /* Logger
16437 ------------------------------------------------------------------------------------------------------------------*/
16438 /**
16439 * @param string $id
16440 * @param bool $prefix_slug
16441 *
16442 * @return FS_Logger
16443 */
16444 function get_logger( $id = '', $prefix_slug = true ) {
16445 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16446 }
16447
16448 /**
16449 * Note: This method is used externally so don't delete it.
16450 *
16451 * @param $id
16452 * @param bool $load_options
16453 * @param bool $prefix_slug
16454 *
16455 * @return FS_Option_Manager
16456 */
16457 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16458 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16459 }
16460
16461 /* Security
16462 ------------------------------------------------------------------------------------------------------------------*/
16463 private static function _encrypt( $str ) {
16464 if ( is_null( $str ) ) {
16465 return null;
16466 }
16467
16468 /**
16469 * The encrypt/decrypt functions are used to protect
16470 * the user from messing up with some of the sensitive
16471 * data stored for the module as a JSON in the database.
16472 *
16473 * I used the same suggested hack by the theme review team.
16474 * For more details, look at the function `Base64UrlDecode()`
16475 * in `./sdk/FreemiusBase.php`.
16476 *
16477 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16478 *
16479 * @author Vova Feldman (@svovaf)
16480 * @since 1.2.2
16481 */
16482 $fn = 'base64' . '_encode';
16483
16484 return $fn( $str );
16485 }
16486
16487 static function _decrypt( $str ) {
16488 if ( is_null( $str ) ) {
16489 return null;
16490 }
16491
16492 /**
16493 * The encrypt/decrypt functions are used to protect
16494 * the user from messing up with some of the sensitive
16495 * data stored for the module as a JSON in the database.
16496 *
16497 * I used the same suggested hack by the theme review team.
16498 * For more details, look at the function `Base64UrlDecode()`
16499 * in `./sdk/FreemiusBase.php`.
16500 *
16501 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16502 *
16503 * @author Vova Feldman (@svovaf)
16504 * @since 1.2.2
16505 */
16506 $fn = 'base64' . '_decode';
16507
16508 return $fn( $str );
16509 }
16510
16511 /**
16512 * @author Vova Feldman (@svovaf)
16513 * @since 1.0.5
16514 *
16515 * @param FS_Entity $entity
16516 *
16517 * @return FS_Entity Return an encrypted clone entity.
16518 */
16519 private static function _encrypt_entity( FS_Entity $entity ) {
16520 $clone = clone $entity;
16521 $props = get_object_vars( $entity );
16522
16523 foreach ( $props as $key => $val ) {
16524 $clone->{$key} = self::_encrypt( $val );
16525 }
16526
16527 return $clone;
16528 }
16529
16530 /**
16531 * @author Vova Feldman (@svovaf)
16532 * @since 1.0.5
16533 *
16534 * @param FS_Entity $entity
16535 *
16536 * @return FS_Entity Return an decrypted clone entity.
16537 */
16538 private static function decrypt_entity( FS_Entity $entity ) {
16539 $clone = clone $entity;
16540 $props = get_object_vars( $entity );
16541
16542 foreach ( $props as $key => $val ) {
16543 $clone->{$key} = self::_decrypt( $val );
16544 }
16545
16546 return $clone;
16547 }
16548
16549 /**
16550 * @author Vova Feldman (@svovaf)
16551 * @since 1.0.7
16552 *
16553 * @param string $email
16554 *
16555 * @return FS_User|false
16556 */
16557 public static function _get_user_by_email( $email ) {
16558 self::$_static_logger->entrance();
16559
16560 $email = trim( strtolower( $email ) );
16561
16562 $users = self::get_all_users();
16563
16564 if ( is_array( $users ) ) {
16565 foreach ( $users as $user ) {
16566 if ( $email === trim( strtolower( $user->email ) ) ) {
16567 return $user;
16568 }
16569 }
16570 }
16571
16572 return false;
16573 }
16574
16575 #----------------------------------------------------------------------------------
16576 #region Account (Loading, Updates & Activation)
16577 #----------------------------------------------------------------------------------
16578
16579 /***
16580 * Load account information (user + site).
16581 *
16582 * @author Vova Feldman (@svovaf)
16583 * @since 1.0.1
16584 */
16585 private function _load_account() {
16586 $this->_logger->entrance();
16587
16588 $this->do_action( 'before_account_load' );
16589
16590 $users = self::get_all_users();
16591 $plans = self::get_all_plans( $this->_module_type );
16592
16593 if ( $this->_logger->is_on() && is_admin() ) {
16594 $this->_logger->log( 'users = ' . var_export( $users, true ) );
16595 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
16596 }
16597
16598 $site = fs_is_network_admin() ?
16599 $this->get_network_install() :
16600 $this->get_install_by_blog_id();
16601
16602 if ( fs_is_network_admin() &&
16603 $this->is_network_active() &&
16604 ! is_object( $site ) &&
16605 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
16606 ) {
16607 $first_install = $this->find_first_install();
16608
16609 if ( is_null( $first_install ) ) {
16610 unset( $this->_storage->network_install_blog_id );
16611 } else {
16612 $site = $first_install['install'];
16613 $this->_storage->network_install_blog_id = $first_install['blog_id'];
16614 }
16615 }
16616
16617 if ( is_object( $site ) &&
16618 is_numeric( $site->id ) &&
16619 is_numeric( $site->user_id ) &&
16620 FS_Plugin_Plan::is_valid_id( $site->plan_id )
16621 ) {
16622 // Load site.
16623 $this->_site = $site;
16624 }
16625
16626 $user = null;
16627 if ( fs_is_network_admin() && $this->_is_network_active ) {
16628 $user = $this->get_network_user();
16629 }
16630
16631 if ( is_object( $user ) ) {
16632 $this->_user = clone $user;
16633 } else if ( $this->_site ) {
16634 $user = self::_get_user_by_id( $this->_site->user_id );
16635
16636 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
16637 /**
16638 * Try to load the previous owner. This recovery is used for the following use-case:
16639 * 1. Opt-in
16640 * 2. Cloning site1 to site2
16641 * 3. Ownership switch in site1 (same applies for site2)
16642 * 4. Install data sync on site2
16643 * 5. Now site2's install is associated with the new owner which does not exists locally.
16644 */
16645 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
16646 }
16647
16648 if ( ! is_object( $user ) ) {
16649 /**
16650 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
16651 */
16652 if (
16653 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
16654 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
16655 ) {
16656 $user = $this->sync_user_by_current_install();
16657 } else {
16658 return;
16659 }
16660
16661 if ( is_object( $user ) ) {
16662 $this->_storage->user_was_recovered_from_install = true;
16663 } else {
16664 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
16665 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
16666 1;
16667
16668 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
16669 $this->delete_current_install( false );
16670 } else {
16671 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
16672
16673 return;
16674 }
16675 }
16676 }
16677
16678 $this->_user = ( $user instanceof FS_User ) ?
16679 clone $user :
16680 null;
16681 }
16682
16683 if ( is_object( $this->_user ) ) {
16684 // Load licenses.
16685 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16686 }
16687
16688 if ( is_object( $this->_site ) ) {
16689 // Load plans.
16690 $this->_plans = isset( $plans[ $this->_slug ] ) ?
16691 $plans[ $this->_slug ] :
16692 array();
16693
16694 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
16695 $this->_sync_plans();
16696 } else {
16697 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16698 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
16699 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
16700 } else {
16701 unset( $this->_plans[ $i ] );
16702 }
16703 }
16704 }
16705
16706 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
16707
16708 if ( $this->_site->version != $this->get_plugin_version() ) {
16709 // If stored install version is different than current installed plugin version,
16710 // then update plugin version event.
16711 $this->update_plugin_version_event();
16712 }
16713 }
16714
16715 if ( true === $this->_storage->require_license_activation &&
16716 ! fs_request_get_bool( 'require_license', true )
16717 ) {
16718 $this->_storage->require_license_activation = false;
16719 }
16720
16721 if ( $this->is_theme() ) {
16722 $this->_register_account_hooks();
16723 }
16724
16725 if ( $this->is_user_in_admin() && $this->is_clone() ) {
16726 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
16727 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
16728 }
16729
16730 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
16731 $this->send_pending_clone_update_once();
16732 }
16733 }
16734
16735 /**
16736 * Special user recovery mechanism.
16737 *
16738 * @author Vova Feldman (@svovaf)
16739 * @since 2.0.0
16740 *
16741 * @param number|null $site_user_id
16742 *
16743 * @return \FS_User|mixed
16744 */
16745 private function sync_user_by_current_install( $site_user_id = null ) {
16746 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
16747 $site_user_id :
16748 $this->_site->user_id;
16749
16750 $api = $this->get_api_site_scope();
16751
16752 $uid = $this->get_anonymous_id();
16753 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
16754
16755 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
16756
16757 if ( $this->is_api_result_entity( $result ) ) {
16758 $user = new FS_User( $result );
16759 $this->_user = $user;
16760 $this->_store_user();
16761
16762 return $user;
16763 }
16764
16765 $error_code = FS_Api::get_error_code( $result );
16766
16767 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
16768 /**
16769 * Those API errors will continue coming and are not recoverable with the
16770 * current site's data. Therefore, extend the API call's cached result to 7 days.
16771 */
16772 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
16773 }
16774
16775 return $result;
16776 }
16777
16778 /**
16779 * @author Vova Feldman (@svovaf)
16780 * @since 1.0.1
16781 *
16782 * @param FS_User $user
16783 * @param FS_Site $site
16784 * @param bool|array $plans
16785 */
16786 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
16787 $site->user_id = $user->id;
16788
16789 $this->_site = $site;
16790 $this->_user = $user;
16791 if ( false !== $plans ) {
16792 $this->_plans = $plans;
16793 }
16794
16795 $this->send_install_update();
16796
16797 $this->_store_account();
16798
16799 }
16800
16801 /**
16802 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
16803 * Each version is trimmed after the 16th char.
16804 *
16805 * @author Vova Feldman (@svovaf)
16806 * @since 2.2.1
16807 *
16808 * @return array
16809 */
16810 private function get_versions() {
16811 $versions = array();
16812 $versions['sdk_version'] = $this->version;
16813
16814 // Collect these diagnostic information only if it's allowed.
16815 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16816 $versions['platform_version'] = get_bloginfo( 'version' );
16817 $versions['programming_language_version'] = phpversion();
16818 }
16819
16820 foreach ( $versions as $k => $version ) {
16821 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
16822 }
16823
16824 return $versions;
16825 }
16826
16827 /**
16828 * Get sanitized site language.
16829 *
16830 * @param string $language
16831 * @param int $max_len
16832 *
16833 * @since 2.5.1
16834 * @author Vova Feldman (@svovaf)
16835 *
16836 * @return string
16837 */
16838 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
16839 if ( empty( $language ) ) {
16840 $language = get_bloginfo( 'language' );
16841 }
16842
16843 return substr( $language, 0, $max_len );
16844 }
16845
16846 /**
16847 * Get core version stripped from pre-release and build.
16848 *
16849 * @since 2.5.1
16850 * @author Vova Feldman (@svovaf)
16851 *
16852 * @param string $version
16853 * @param int $parts
16854 * @param int $max_len
16855 * @param bool $include_pre_release
16856 *
16857 * @return string
16858 */
16859 private static function get_core_version(
16860 $version,
16861 $parts = 3,
16862 $max_len = self::VERSION_MAX_CHARS,
16863 $include_pre_release = false
16864 ) {
16865 if ( empty( $version ) ) {
16866 // Version is empty.
16867 return '';
16868 }
16869
16870 if ( is_numeric( $version ) ) {
16871 $is_float_version = is_float( $version );
16872
16873 $version = (string) $version;
16874
16875 /**
16876 * Casting a whole float number to a string cuts the decimal point. This part make sure to add the missing decimal part to the version.
16877 */
16878 if ( $is_float_version && false === strpos( $version, '.' ) ) {
16879 $version .= '.0';
16880 }
16881 }
16882
16883 if ( ! is_string( $version ) ) {
16884 return '';
16885 }
16886
16887 if ( $parts < 1 ) {
16888 return '';
16889 }
16890
16891 $pre_release_regex = $include_pre_release ?
16892 '(\-(alpha|beta|RC)([0-9]+)?)?' :
16893 '';
16894
16895 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
16896 // Version is not starting with a digit.
16897 return '';
16898 }
16899
16900 return substr( $matches[1], 0, $max_len );
16901 }
16902
16903 /**
16904 * @param string $prop
16905 * @param mixed $val
16906 *
16907 * @return mixed
16908 *@author Vova Feldman (@svovaf)
16909 *
16910 * @since 2.5.1
16911 */
16912 private static function get_api_sanitized_property( $prop, $val ) {
16913 if ( ! is_string( $val ) || empty( $val ) ) {
16914 return $val;
16915 }
16916
16917 switch ( $prop ) {
16918 case 'programming_language_version':
16919 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
16920 return self::get_core_version( $val );
16921 case 'platform_version':
16922 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
16923 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
16924 case 'sdk_version':
16925 // Get the exact SDK version, which can have up to 4 parts.
16926 return self::get_core_version( $val, 4 );
16927 case 'version':
16928 // Get the entire version but just limited in length.
16929 return substr( $val, 0, self::VERSION_MAX_CHARS );
16930 case 'language':
16931 return self::get_sanitized_language( $val );
16932 default:
16933 return $val;
16934 }
16935 }
16936
16937 /**
16938 * @author Leo Fajardo (@leorw)
16939 * @since 2.3.0
16940 *
16941 * @return bool
16942 */
16943 function has_beta_update() {
16944 return (
16945 ! empty( $this->_storage->beta_data ) &&
16946 ( true === $this->_storage->beta_data['is_beta'] ) &&
16947 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
16948 );
16949 }
16950
16951 /**
16952 * @author Leo Fajardo (@leorw)
16953 * @since 2.3.0
16954 *
16955 * @return bool
16956 */
16957 function is_beta() {
16958 return (
16959 ! empty( $this->_storage->beta_data ) &&
16960 ( true === $this->_storage->beta_data['is_beta'] ) &&
16961 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
16962 );
16963 }
16964
16965 /**
16966 * @author Vova Feldman (@svovaf)
16967 * @since 1.1.7.4
16968 *
16969 * @param array $override_with
16970 * @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.
16971 * @param bool $skip_user_info
16972 *
16973 * @return array
16974 */
16975 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null, $skip_user_info = false ) {
16976 $this->_logger->entrance();
16977
16978 $activation_action = $this->get_unique_affix() . '_activate_new';
16979 $return_url = $this->is_anonymous() ?
16980 // If skipped already, then return to the account page.
16981 $this->get_account_url( $activation_action, array(), false ) :
16982 // Return to the module's main page.
16983 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
16984
16985 $versions = $this->get_versions();
16986
16987 $params = array_merge( $versions, array(
16988 'plugin_slug' => $this->_slug,
16989 'plugin_id' => $this->get_id(),
16990 'plugin_public_key' => $this->get_public_key(),
16991 'plugin_version' => $this->get_plugin_version(),
16992 'return_url' => fs_nonce_url( $return_url, $activation_action ),
16993 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
16994 'account',
16995 array( 'fs_action' => 'sync_user' )
16996 ), 'sync_user' ),
16997 'is_premium' => $this->is_premium(),
16998 'is_active' => true,
16999 'is_uninstalled' => false,
17000 'is_localhost' => WP_FS__IS_LOCALHOST,
17001 ) );
17002
17003 if (
17004 ! $skip_user_info &&
17005 (
17006 empty( $override_with['user_firstname'] ) ||
17007 empty( $override_with['user_lastname'] ) ||
17008 empty( $override_with['user_email'] )
17009 )
17010 ) {
17011 $current_user = self::_get_current_wp_user();
17012
17013 $params['user_firstname'] = $current_user->user_firstname;
17014 $params['user_lastname'] = $current_user->user_lastname;
17015 $params['user_email'] = $current_user->user_email;
17016 }
17017
17018 if ( $this->is_addon() ) {
17019 $parent_fs = $this->get_parent_instance();
17020
17021 $params['parent_plugin_slug'] = $parent_fs->_slug;
17022 $params['parent_plugin_id'] = $parent_fs->get_id();
17023 }
17024
17025 if ( true === $network_level_or_blog_id ) {
17026 if ( ! isset( $override_with['sites'] ) ) {
17027 $params['sites'] = $this->get_sites_for_network_level_optin();
17028 }
17029 } else {
17030 $site = is_numeric( $network_level_or_blog_id ) ?
17031 array( 'blog_id' => $network_level_or_blog_id ) :
17032 null;
17033
17034 $site = $this->get_site_info( $site );
17035
17036 $diagnostic_info = array();
17037 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17038 $diagnostic_info = array(
17039 'site_name' => $site['title'],
17040 'language' => self::get_sanitized_language( $site['language'] ),
17041 );
17042 }
17043
17044 $params = array_merge( $params, $diagnostic_info, array(
17045 'site_uid' => $site['uid'],
17046 'site_url' => $site['url'],
17047 ) );
17048 }
17049
17050 if ( $this->is_pending_activation() &&
17051 ! empty( $this->_storage->pending_license_key )
17052 ) {
17053 $params['license_key'] = $this->_storage->pending_license_key;
17054 }
17055
17056 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17057 // Even though rand() is known for its security issues,
17058 // the timestamp adds another layer of protection.
17059 // It would be very hard for an attacker to get the secret key form here.
17060 // Plus, this should never run in production since the secret should never
17061 // be included in the production version.
17062 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17063 $params['salt'] = md5( uniqid( rand() ) );
17064 $params['secure'] = md5(
17065 $params['ts'] .
17066 $params['salt'] .
17067 $this->get_secret_key()
17068 );
17069 }
17070
17071 if ( is_multisite() && function_exists( 'get_network' ) ) {
17072 $params['network_uid'] = $this->get_anonymous_network_id();
17073 }
17074
17075 return array_merge( $params, $override_with );
17076 }
17077
17078 /**
17079 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17080 * 2. If there was an API error, return the API result.
17081 *
17082 * @author Vova Feldman (@svovaf)
17083 * @since 1.1.7.4
17084 *
17085 * @param string|bool $email
17086 * @param string|bool $first
17087 * @param string|bool $last
17088 * @param string|bool $license_key
17089 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17090 * In this case, the user and site info will be sent to the server but no
17091 * data will be saved to the WP installation's database.
17092 * @param number|bool $trial_plan_id
17093 * @param bool $is_disconnected Whether to opt in without tracking.
17094 * @param null|bool $is_marketing_allowed
17095 * @param array $sites If network-level opt-in, an array of containing details of sites.
17096 * @param bool $redirect
17097 * @param null|number $license_owner_id
17098 *
17099 * @return string|object
17100 * @use WP_Error
17101 */
17102 function opt_in(
17103 $email = false,
17104 $first = false,
17105 $last = false,
17106 $license_key = false,
17107 $is_uninstall = false,
17108 $trial_plan_id = false,
17109 $is_disconnected = false,
17110 $is_marketing_allowed = null,
17111 $sites = array(),
17112 $redirect = true,
17113 $license_owner_id = null
17114 ) {
17115 $this->_logger->entrance();
17116
17117 /**
17118 * @since 1.2.1 If activating with license key, ignore the context-user
17119 * since the user will be automatically loaded from the license.
17120 */
17121 if ( empty( $license_key ) ) {
17122 // Clean up pending license if opt-ing in again.
17123 $this->_storage->remove( 'pending_license_key' );
17124
17125 if ( ! $is_uninstall ) {
17126 if ( false === $email ) {
17127 $current_user = self::_get_current_wp_user();
17128 $email = $current_user->user_email;
17129 }
17130
17131 $fs_user = Freemius::_get_user_by_email( $email );
17132 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17133 return $this->install_with_user(
17134 $fs_user,
17135 false,
17136 $trial_plan_id,
17137 $redirect,
17138 true,
17139 $sites
17140 );
17141 }
17142 }
17143 }
17144
17145 $skip_user_info = ( ! empty( $license_key ) && FS_User::is_valid_id( $license_owner_id ) );
17146
17147 $user_info = array();
17148
17149 if ( ! $skip_user_info ) {
17150 if ( ! empty( $email ) ) {
17151 $user_info['user_email'] = $email;
17152 }
17153
17154 if ( ! empty( $first ) ) {
17155 $user_info['user_firstname'] = $first;
17156 }
17157
17158 if ( ! empty( $last ) ) {
17159 $user_info['user_lastname'] = $last;
17160 }
17161 }
17162
17163 if ( ! empty( $sites ) ) {
17164 $is_network = true;
17165
17166 $user_info['sites'] = $sites;
17167 } else {
17168 $is_network = false;
17169 }
17170
17171 $params = $this->get_opt_in_params( $user_info, $is_network, $skip_user_info );
17172
17173 $filtered_license_key = false;
17174 if ( is_string( $license_key ) ) {
17175 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17176 $params['license_key'] = $filtered_license_key;
17177 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17178 $params['trial_plan_id'] = $trial_plan_id;
17179 }
17180
17181 if ( $is_uninstall ) {
17182 $params['uninstall_params'] = array(
17183 'reason_id' => $this->_storage->uninstall_reason->id,
17184 'reason_info' => $this->_storage->uninstall_reason->info
17185 );
17186 }
17187
17188 if ( isset( $params['license_key'] ) ) {
17189 $fs_user = Freemius::_get_user_by_email( $email );
17190
17191 if ( is_object( $fs_user ) ) {
17192 /**
17193 * If opting in with a context license and the context WP Admin user already opted in
17194 * before from the current site, add the user context security params to avoid the
17195 * unnecessary email activation when the context license is owned by the same context user.
17196 *
17197 * @author Leo Fajardo (@leorw)
17198 * @since 1.2.3
17199 */
17200 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17201 $fs_user,
17202 false,
17203 'install_with_existing_user'
17204 ) );
17205 }
17206 }
17207
17208 if ( is_bool( $is_marketing_allowed ) ) {
17209 $params['is_marketing_allowed'] = $is_marketing_allowed;
17210 }
17211
17212 $params['is_disconnected'] = $is_disconnected;
17213 $params['format'] = 'json';
17214 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17215 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17216
17217 $request = array(
17218 'method' => 'POST',
17219 'body' => $params,
17220 'timeout' => 60,
17221 );
17222
17223 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17224 $response = self::safe_remote_post( $url, $request );
17225
17226 if ( is_wp_error( $response ) ) {
17227 /**
17228 * @var WP_Error $response
17229 */
17230 $result = new stdClass();
17231
17232 $error_code = $response->get_error_code();
17233 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17234
17235 $result->error = (object) array(
17236 'type' => $error_type,
17237 'message' => $response->get_error_message(),
17238 'code' => $error_code,
17239 'http' => 402
17240 );
17241
17242 $this->maybe_modify_api_curl_error_message( $result );
17243
17244 if ( FS_Api::is_blocked( $result ) ) {
17245 $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
17246 }
17247
17248 $is_connected = null;
17249
17250 if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
17251 $this->skip_connection( fs_is_network_admin() );
17252
17253 $is_connected = ( ! FS_Api::is_blocked( $result ) );
17254 }
17255
17256 $this->update_connectivity_info( $is_connected );
17257
17258 return $result;
17259 }
17260
17261 $this->update_connectivity_info( true );
17262
17263 // Module is being uninstalled, don't handle the returned data.
17264 if ( $is_uninstall ) {
17265 return true;
17266 }
17267
17268 /**
17269 * 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.
17270 *
17271 * @author Vova Feldman (@svovaf)
17272 * @since 1.2.3
17273 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17274 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17275 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17276 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17277 */
17278 $decoded = is_string( $response['body'] ) ?
17279 json_decode( $response['body'] ) :
17280 null;
17281
17282 if ( empty( $decoded ) ) {
17283 return false;
17284 }
17285
17286 if ( ! $this->is_api_result_object( $decoded ) ) {
17287 if ( ! empty( $params['license_key'] ) ) {
17288 // Pass the fully entered license key to the failure handler.
17289 $params['license_key'] = $license_key;
17290 }
17291
17292 return $is_uninstall ?
17293 $decoded :
17294 $this->apply_filters( 'after_install_failure', $decoded, $params );
17295 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17296 if ( $is_network ) {
17297 $site_ids = array();
17298 foreach ( $sites as $site ) {
17299 $site_ids[] = $site['blog_id'];
17300 }
17301
17302 /**
17303 * Store the sites so that they can be installed once the user has clicked on the activation link
17304 * in the email.
17305 *
17306 * @author Leo Fajardo (@leorw)
17307 */
17308 $this->_storage->pending_sites_info = array(
17309 'blog_ids' => $site_ids,
17310 'license_key' => $license_key,
17311 'trial_plan_id' => $trial_plan_id
17312 );
17313 }
17314
17315 // Pending activation, add message.
17316 return $this->set_pending_confirmation(
17317 ( isset( $decoded->email ) ?
17318 $decoded->email :
17319 true ),
17320 false,
17321 $filtered_license_key,
17322 ! empty( $params['trial_plan_id'] ),
17323 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17324 );
17325 } else if ( isset( $decoded->install_secret_key ) ) {
17326 return $this->install_with_new_user(
17327 $decoded->user_id,
17328 $decoded->user_public_key,
17329 $decoded->user_secret_key,
17330 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17331 $decoded->is_marketing_allowed :
17332 null ),
17333 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17334 $decoded->is_extensions_tracking_allowed :
17335 null ),
17336 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17337 $decoded->is_diagnostic_tracking_allowed :
17338 null ),
17339 $decoded->install_id,
17340 $decoded->install_public_key,
17341 $decoded->install_secret_key,
17342 false
17343 );
17344 } else if ( is_array( $decoded->installs ) ) {
17345 return $this->install_many_with_new_user(
17346 $decoded->user_id,
17347 $decoded->user_public_key,
17348 $decoded->user_secret_key,
17349 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17350 $decoded->is_marketing_allowed :
17351 null ),
17352 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17353 $decoded->is_extensions_tracking_allowed :
17354 null ),
17355 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17356 $decoded->is_diagnostic_tracking_allowed :
17357 null ),
17358 $decoded->installs,
17359 false
17360 );
17361 }
17362
17363 return $decoded;
17364 }
17365
17366 /**
17367 * Set user and site identities.
17368 *
17369 * @author Vova Feldman (@svovaf)
17370 * @since 1.0.9
17371 *
17372 * @param FS_User $user
17373 * @param FS_Site $site
17374 * @param bool $redirect
17375 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17376 * redirect (or return a URL) to the account page with a special parameter to
17377 * trigger the auto installation processes.
17378 *
17379 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17380 */
17381 function setup_account(
17382 FS_User $user,
17383 FS_Site $site,
17384 $redirect = true,
17385 $auto_install = false
17386 ) {
17387 return $this->setup_network_account(
17388 $user,
17389 array( $site ),
17390 $redirect,
17391 $auto_install,
17392 false
17393 );
17394 }
17395
17396 /**
17397 * Set user and site identities.
17398 *
17399 * @author Vova Feldman (@svovaf)
17400 * @since 2.0.0
17401 *
17402 * @param FS_User $user
17403 * @param FS_Site[] $installs
17404 * @param bool $redirect
17405 * @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.
17406 * @param bool $is_network_level_opt_in
17407 *
17408 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17409 */
17410 function setup_network_account(
17411 FS_User $user,
17412 array $installs,
17413 $redirect = true,
17414 $auto_install = false,
17415 $is_network_level_opt_in = true
17416 ) {
17417 $first_install = $installs[0];
17418
17419 $this->_user = $user;
17420 $this->_site = $first_install;
17421
17422 $this->_sync_plans();
17423
17424 if ( $this->_storage->handle_gdpr_admin_notice &&
17425 $this->should_handle_gdpr_admin_notice() &&
17426 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17427 ) {
17428 /**
17429 * Clear user lock after an opt-in.
17430 */
17431 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17432 FS_User_Lock::instance()->unlock();
17433 }
17434
17435 if ( 1 < count( $installs ) ) {
17436 // Only network level opt-in can have more than one install.
17437 $is_network_level_opt_in = true;
17438 }
17439
17440 $this->update_connectivity_info( true );
17441
17442 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17443 // If Freemius was OFF before, turn it on.
17444 $this->turn_on();
17445
17446 $this->handle_account_connection(
17447 $installs,
17448 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17449 );
17450
17451 if ( is_numeric( $first_install->license_id ) ) {
17452 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17453 }
17454
17455 $this->_admin_notices->remove_sticky( 'connect_account' );
17456
17457 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17458 $this->clear_pending_activation_mode();
17459
17460 if ( ! $this->is_paying_or_trial() ) {
17461 $this->_admin_notices->add_sticky(
17462 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17463 'activation_complete'
17464 );
17465 }
17466 }
17467
17468 if ( $this->is_paying_or_trial() ) {
17469 if ( ! $this->is_premium() ||
17470 ! $this->has_premium_version() ||
17471 ! $this->has_settings_menu()
17472 ) {
17473 if ( $this->is_paying() ) {
17474 $this->add_complete_upgrade_instructions_notice(
17475 sprintf(
17476 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17477 $this->get_plan_title()
17478 ),
17479 'plan_upgraded'
17480 );
17481 } else {
17482 $trial_plan = $this->get_trial_plan();
17483
17484 $this->add_complete_upgrade_instructions_notice(
17485 sprintf(
17486 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17487 '<i>' . $this->get_plugin_name() . '</i>'
17488 ),
17489 'trial_started',
17490 $trial_plan->title
17491 );
17492 }
17493 }
17494
17495 $this->_admin_notices->remove_sticky( array(
17496 'trial_promotion',
17497 ) );
17498 }
17499
17500 $plugin_id = fs_request_get( 'plugin_id', false );
17501
17502 // Store activation time ONLY for plugins & themes (not add-ons).
17503 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17504 if ( empty( $this->_storage->activation_timestamp ) ) {
17505 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17506 }
17507 }
17508
17509 $next_page = '';
17510
17511 $extra = array();
17512 if ( $auto_install ) {
17513 $extra['auto_install'] = 'true';
17514 }
17515
17516 if ( is_numeric( $plugin_id ) ) {
17517 /**
17518 * @author Leo Fajardo (@leorw)
17519 * @since 1.2.1.6
17520 *
17521 * Also sync the license after an anonymous user subscribes.
17522 */
17523 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17524 // Add-on was installed - sync license right after install.
17525 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17526 }
17527 } else {
17528 /**
17529 * @author Vova Feldman (@svovaf)
17530 * @since 1.1.9 If site installed with a valid license, sync license.
17531 */
17532 if ( $this->is_paying() ) {
17533 $this->_sync_plugin_license(
17534 true,
17535 // Installs data is already synced in the beginning of this method directly or via _set_account().
17536 false
17537 );
17538 }
17539
17540 // Reload the page with the keys.
17541 $next_page = $this->is_anonymous() ?
17542 // If user previously skipped, redirect to account page.
17543 $this->get_account_url( false, $extra ) :
17544 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17545 }
17546
17547 if ( ! empty( $next_page ) && $redirect ) {
17548 fs_redirect( $next_page );
17549 }
17550
17551 return $next_page;
17552 }
17553
17554 /**
17555 * Install plugin with new user information after approval.
17556 *
17557 * @author Vova Feldman (@svovaf)
17558 * @since 1.0.7
17559 */
17560 function _install_with_new_user() {
17561 $this->_logger->entrance();
17562
17563 if ( $this->is_registered() ) {
17564 return;
17565 }
17566
17567 $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' );
17568
17569 $this->update_license_required_permissions_if_anonymous();
17570
17571 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17572 // @todo This logic should be improved because it's executed on every load of a theme.
17573 $this->is_theme()
17574 ) {
17575 // check_admin_referer( $this->_slug . '_activate_new' );
17576
17577 if ( fs_request_has( 'user_secret_key' ) ) {
17578 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17579 $pending_sites_info = $this->_storage->pending_sites_info;
17580
17581 $this->install_many_pending_with_user(
17582 fs_request_get( 'user_id' ),
17583 fs_request_get_raw( 'user_public_key' ),
17584 fs_request_get_raw( 'user_secret_key' ),
17585 fs_request_get_bool( 'is_marketing_allowed', null ),
17586 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17587 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17588 $pending_sites_info['blog_ids'],
17589 $pending_sites_info['license_key'],
17590 $pending_sites_info['trial_plan_id']
17591 );
17592 } else {
17593 $this->install_with_new_user(
17594 fs_request_get( 'user_id' ),
17595 fs_request_get_raw( 'user_public_key' ),
17596 fs_request_get_raw( 'user_secret_key' ),
17597 fs_request_get_bool( 'is_marketing_allowed', null ),
17598 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17599 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17600 fs_request_get( 'install_id' ),
17601 fs_request_get_raw( 'install_public_key' ),
17602 fs_request_get_raw( 'install_secret_key' ),
17603 true,
17604 fs_request_get_bool( 'auto_install' )
17605 );
17606 }
17607 } else if ( $has_pending_activation_confirmation_param ) {
17608 $this->set_pending_confirmation(
17609 fs_request_get( 'user_email' ),
17610 true,
17611 false,
17612 false,
17613 fs_request_get_bool( 'is_suspicious_email' ),
17614 fs_request_get_bool( 'has_upgrade_context' ),
17615 fs_request_get( 'support_email_address' )
17616 );
17617 }
17618 }
17619 }
17620
17621 /**
17622 * @author Vova Feldman (@svovaf)
17623 * @since 2.0.0
17624 *
17625 * @param number $id
17626 * @param string $public_key
17627 * @param string $secret_key
17628 *
17629 * @return \FS_User
17630 */
17631 private function setup_user( $id, $public_key, $secret_key ) {
17632 $user = self::_get_user_by_id( $id );
17633
17634 if ( is_object( $user ) ) {
17635 $this->_user = $user;
17636 } else {
17637 $user = new FS_User();
17638 $user->id = $id;
17639 $user->public_key = $public_key;
17640 $user->secret_key = $secret_key;
17641
17642 $this->_user = $user;
17643 $user_result = $this->get_api_user_scope()->get();
17644 $user = new FS_User( $user_result );
17645
17646 $this->_user = $user;
17647 $this->_store_user();
17648 }
17649
17650 return $user;
17651 }
17652
17653 /**
17654 * Install plugin with new user.
17655 *
17656 * @author Vova Feldman (@svovaf)
17657 * @since 1.1.7.4
17658 *
17659 * @param number $user_id
17660 * @param string $user_public_key
17661 * @param string $user_secret_key
17662 * @param bool|null $is_marketing_allowed
17663 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17664 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17665 * @param number $install_id
17666 * @param string $install_public_key
17667 * @param string $install_secret_key
17668 * @param bool $redirect
17669 * @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.
17670 *
17671 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17672 */
17673 private function install_with_new_user(
17674 $user_id,
17675 $user_public_key,
17676 $user_secret_key,
17677 $is_marketing_allowed,
17678 $is_extensions_tracking_allowed,
17679 $is_diagnostic_tracking_allowed,
17680 $install_id,
17681 $install_public_key,
17682 $install_secret_key,
17683 $redirect = true,
17684 $auto_install = false
17685 ) {
17686 /**
17687 * This method is also executed after opting in with a license key since the
17688 * license can be potentially associated with a different owner.
17689 *
17690 * @since 2.0.0
17691 */
17692 $user = self::_get_user_by_id( $user_id );
17693
17694 if ( ! is_object( $user ) ) {
17695 $user = new FS_User();
17696 $user->id = $user_id;
17697 $user->public_key = $user_public_key;
17698 $user->secret_key = $user_secret_key;
17699
17700 $this->_user = $user;
17701 $user_result = $this->get_api_user_scope()->get();
17702 $user = new FS_User( $user_result );
17703 }
17704
17705 $this->_user = $user;
17706
17707 $site = new FS_Site();
17708 $site->id = $install_id;
17709 $site->public_key = $install_public_key;
17710 $site->secret_key = $install_secret_key;
17711
17712 $this->_site = $site;
17713 $site_result = $this->get_api_site_scope( true )->get();
17714 $site = new FS_Site( $site_result );
17715 $this->_site = $site;
17716
17717 if ( ! is_null( $is_marketing_allowed ) ) {
17718 $this->disable_opt_in_notice_and_lock_user();
17719 }
17720
17721 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17722 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17723 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17724 ) );
17725
17726 return $this->setup_account(
17727 $this->_user,
17728 $this->_site,
17729 $redirect,
17730 $auto_install
17731 );
17732 }
17733
17734 /**
17735 * Install plugin with user.
17736 *
17737 * @author Leo Fajardo (@leorw)
17738 * @since 2.0.0
17739 *
17740 * @param number $user_id
17741 * @param string $user_public_key
17742 * @param string $user_secret_key
17743 * @param bool|null $is_marketing_allowed
17744 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17745 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17746 * @param array $site_ids
17747 * @param bool $license_key
17748 * @param bool $trial_plan_id
17749 * @param bool $redirect
17750 *
17751 * @return void
17752 */
17753 private function install_many_pending_with_user(
17754 $user_id,
17755 $user_public_key,
17756 $user_secret_key,
17757 $is_marketing_allowed,
17758 $is_extensions_tracking_allowed,
17759 $is_diagnostic_tracking_allowed,
17760 $site_ids,
17761 $license_key = false,
17762 $trial_plan_id = false,
17763 $redirect = true
17764 ) {
17765 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17766
17767 if ( ! is_null( $is_marketing_allowed ) ) {
17768 $this->disable_opt_in_notice_and_lock_user();
17769 }
17770
17771 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17772 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17773 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17774 ) );
17775
17776 $sites = array();
17777 foreach ( $site_ids as $site_id ) {
17778 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
17779 }
17780
17781 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17782 }
17783
17784 /**
17785 * Multi-site install with a new user.
17786 *
17787 * @author Vova Feldman (@svovaf)
17788 * @since 2.0.0
17789 *
17790 * @param number $user_id
17791 * @param string $user_public_key
17792 * @param string $user_secret_key
17793 * @param bool|null $is_marketing_allowed
17794 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
17795 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
17796 * @param object[] $installs
17797 * @param bool $redirect
17798 * @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.
17799 *
17800 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17801 */
17802 private function install_many_with_new_user(
17803 $user_id,
17804 $user_public_key,
17805 $user_secret_key,
17806 $is_marketing_allowed,
17807 $is_extensions_tracking_allowed,
17808 $is_diagnostic_tracking_allowed,
17809 array $installs,
17810 $redirect = true,
17811 $auto_install = false
17812 ) {
17813 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
17814
17815 if ( ! is_null( $is_marketing_allowed ) ) {
17816 $this->disable_opt_in_notice_and_lock_user();
17817 }
17818
17819 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17820 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
17821 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
17822 ) );
17823
17824 $install_ids = array();
17825
17826 foreach ( $installs as $install ) {
17827 $install_ids[] = $install->id;
17828 }
17829
17830 $items_per_request = 25;
17831 $left = count( $install_ids );
17832 $offset = 0;
17833
17834 $installs = array();
17835 while ( $left > 0 ) {
17836 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
17837
17838 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
17839 // @todo Handle API error.
17840 }
17841
17842 $installs = array_merge( $installs, $result->installs );
17843
17844 $left -= $items_per_request;
17845 $offset += $items_per_request;
17846 }
17847
17848 foreach ( $installs as &$install ) {
17849 $install = new FS_Site( $install );
17850 }
17851
17852 return $this->setup_network_account(
17853 $this->_user,
17854 $installs,
17855 $redirect,
17856 $auto_install
17857 );
17858 }
17859
17860 /**
17861 * @author Vova Feldman (@svovaf)
17862 * @since 1.1.7.4
17863 *
17864 * @param string|bool $email
17865 * @param bool $redirect
17866 * @param string|bool $license_key Since 1.2.1.5
17867 * @param bool $is_pending_trial Since 1.2.1.5
17868 * @param bool $is_suspicious_email Since 2.5.0
17869 * @param bool $has_upgrade_context Since 2.5.3
17870 * @param bool|string $support_email_address Since 2.5.3
17871 *
17872 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
17873 */
17874 private function set_pending_confirmation(
17875 $email = false,
17876 $redirect = true,
17877 $license_key = false,
17878 $is_pending_trial = false,
17879 $is_suspicious_email = false,
17880 $has_upgrade_context = false,
17881 $support_email_address = false
17882 ) {
17883 $is_network_admin = fs_is_network_admin();
17884
17885 if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) {
17886 /**
17887 * If explicitly asked to ignore pending mode, set to anonymous mode
17888 * if require confirmation before finalizing the opt-in except after completing a purchase (otherwise, in this case, they wouldn't see any notice telling them that they should receive their license key via email).
17889 *
17890 * @author Vova Feldman
17891 * @since 1.2.1.6
17892 */
17893 $this->skip_connection( $is_network_admin );
17894 } else {
17895 // Install must be activated via email since
17896 // user with the same email already exist.
17897 $this->_storage->is_pending_activation = true;
17898 $this->_add_pending_activation_notice(
17899 $email,
17900 $is_pending_trial,
17901 $is_suspicious_email,
17902 $has_upgrade_context,
17903 $support_email_address
17904 );
17905 }
17906
17907 if ( ! empty( $license_key ) ) {
17908 $this->_storage->pending_license_key = $license_key;
17909 }
17910
17911 // Remove the opt-in sticky notice.
17912 $this->_admin_notices->remove_sticky( array(
17913 'connect_account',
17914 'trial_promotion',
17915 ) );
17916
17917 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
17918
17919 if ( $redirect ) {
17920 // Reload the page with a pending activation message.
17921 fs_redirect( $next_page );
17922 }
17923
17924 return $next_page;
17925 }
17926
17927 /**
17928 * Install plugin with current logged WP user info.
17929 *
17930 * @author Vova Feldman (@svovaf)
17931 * @since 1.0.7
17932 */
17933 function _install_with_current_user() {
17934 $this->_logger->entrance();
17935
17936 if ( $this->is_registered() ) {
17937 return;
17938 }
17939
17940 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
17941 check_admin_referer( $this->get_unique_affix() . '_activate_existing' );
17942
17943 /**
17944 * @author Vova Feldman (@svovaf)
17945 * @since 1.1.9 Add license key if given.
17946 */
17947 $license_key = fs_request_get_raw( 'license_secret_key' );
17948
17949 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
17950 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17951 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17952 ) );
17953
17954 $this->install_with_current_user( $license_key );
17955 }
17956 }
17957
17958
17959 /**
17960 * @author Vova Feldman (@svovaf)
17961 * @since 1.1.7.4
17962 *
17963 * @param string|bool $license_key
17964 * @param number|bool $trial_plan_id
17965 * @param array $sites Since 2.0.0
17966 * @param bool $redirect
17967 *
17968 * @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.
17969 */
17970 function install_with_current_user(
17971 $license_key = false,
17972 $trial_plan_id = false,
17973 $sites = array(),
17974 $redirect = true
17975 ) {
17976 // Get current logged WP user.
17977 $current_user = self::_get_current_wp_user();
17978
17979 // Find the relevant FS user by the email.
17980 $user = self::_get_user_by_email( $current_user->user_email );
17981
17982 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
17983 }
17984
17985 /**
17986 * @author Vova Feldman (@svovaf)
17987 * @since 2.0.0
17988 *
17989 * @param \FS_User $user
17990 * @param string|bool $license_key
17991 * @param number|bool $trial_plan_id
17992 * @param bool $redirect
17993 * @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.
17994 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
17995 *
17996 * @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.
17997 */
17998 function install_with_user(
17999 FS_User $user,
18000 $license_key = false,
18001 $trial_plan_id = false,
18002 $redirect = true,
18003 $setup_account = true,
18004 $sites = array()
18005 ) {
18006 // We have to set the user before getting user scope API handler.
18007 $this->_user = $user;
18008
18009 // Install the plugin.
18010 $result = $this->create_installs_with_user(
18011 $user,
18012 $license_key,
18013 $trial_plan_id,
18014 $sites,
18015 $redirect
18016 );
18017
18018 if ( ! $this->is_api_result_entity( $result ) &&
18019 ! $this->is_api_result_object( $result, 'installs' )
18020 ) {
18021 // @todo Handler potential API error of the $result
18022 }
18023
18024 if ( empty( $sites ) ) {
18025 $site = new FS_Site( $result );
18026 $this->_site = $site;
18027
18028 if ( ! $setup_account ) {
18029 $this->_store_site();
18030
18031 $this->sync_plan_if_not_exist( $site->plan_id );
18032
18033 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18034 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18035 }
18036
18037 $this->_admin_notices->remove_sticky( 'connect_account', false );
18038
18039 return $site;
18040 }
18041
18042 return $this->setup_account( $this->_user, $this->_site, $redirect );
18043 } else {
18044 $installs = array();
18045 foreach ( $result->installs as $install ) {
18046 $installs[] = new FS_Site( $install );
18047 }
18048
18049 return $this->setup_network_account(
18050 $user,
18051 $installs,
18052 $redirect
18053 );
18054 }
18055 }
18056
18057 /**
18058 * Initiate an API request to create a collection of installs.
18059 *
18060 * @author Vova Feldman (@svovaf)
18061 * @since 2.0.0
18062 *
18063 * @param \FS_User $user
18064 * @param bool $license_key
18065 * @param bool $trial_plan_id
18066 * @param array $sites
18067 * @param bool $redirect
18068 * @param bool $silent
18069 *
18070 * @return object|mixed
18071 */
18072 private function create_installs_with_user(
18073 FS_User $user,
18074 $license_key = false,
18075 $trial_plan_id = false,
18076 $sites = array(),
18077 $redirect = false,
18078 $silent = false
18079 ) {
18080 $extra_install_params = array(
18081 'uid' => $this->get_anonymous_id(),
18082 'is_disconnected' => false,
18083 );
18084
18085 if ( ! empty( $license_key ) ) {
18086 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18087
18088 if ( $silent ) {
18089 $extra_install_params['ignore_license_owner'] = true;
18090 }
18091 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18092 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18093 }
18094
18095 if ( ! empty( $sites ) ) {
18096 $extra_install_params['sites'] = $sites;
18097 }
18098
18099 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18100
18101 // Install the plugin.
18102 $result = $this->get_api_user_scope_by_user( $user )->call(
18103 "/plugins/{$this->get_id()}/installs.json",
18104 'post',
18105 $args
18106 );
18107
18108 if ( ! $this->is_api_result_entity( $result ) &&
18109 ! $this->is_api_result_object( $result, 'installs' )
18110 ) {
18111 if ( ! empty( $args['license_key'] ) ) {
18112 // Pass the fully entered license key to the failure handler.
18113 $args['license_key'] = $license_key;
18114 }
18115
18116 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18117
18118 if ( ! $silent ) {
18119 $this->_admin_notices->add(
18120 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18121 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18122 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18123 'error'
18124 );
18125 }
18126
18127 if ( $redirect ) {
18128 /**
18129 * We set the user before getting the user scope API handler, so the user became temporarily
18130 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18131 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18132 * activation page based on the return value of `is_registered()`. In addition, in case the
18133 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18134 * misleading plugin activation errors will be shown on the `Plugins` page.
18135 *
18136 * @author Leo Fajardo (@leorw)
18137 */
18138 $user = $this->_user;
18139
18140 $this->_user = null;
18141
18142 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18143
18144 /**
18145 * Restore the user after the redirect, this is relevant when there are cases where the redirect will choose not to do anything.
18146 */
18147 $this->_user = $user;
18148 }
18149 }
18150
18151 return $result;
18152 }
18153
18154 /**
18155 * Tries to activate add-on account based on parent plugin info.
18156 *
18157 * @author Vova Feldman (@svovaf)
18158 * @since 1.0.6
18159 *
18160 * @param Freemius $parent_fs
18161 * @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.
18162 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18163 */
18164 private function _activate_addon_account(
18165 Freemius $parent_fs,
18166 $network_level_or_blog_id = null,
18167 $bundle_license = null
18168 ) {
18169 if ( $this->is_registered() ) {
18170 // Already activated.
18171 return;
18172 }
18173
18174 $permission_ids = FS_Permission_Manager::get_all_permission_ids();
18175 $permissions = array();
18176 foreach ( $permission_ids as $permission_id ) {
18177 $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true );
18178 }
18179
18180 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions );
18181
18182 /**
18183 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18184 * already returns the data for the current blog.
18185 *
18186 * @author Leo Fajardo (@leorw)
18187 * @since 2.3.0
18188 */
18189 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18190 array() :
18191 array( 'uid' => $this->get_anonymous_id() );
18192
18193 $params = $this->get_install_data_for_api(
18194 $uid_param_to_override,
18195 false,
18196 false,
18197 /**
18198 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18199 * already includes the data for it.
18200 *
18201 * @author Leo Fajardo (@leorw)
18202 * @since 2.3.0
18203 */
18204 ( true !== $network_level_or_blog_id )
18205 );
18206
18207 if ( true === $network_level_or_blog_id ) {
18208 $params['sites'] = $this->get_sites_for_network_level_optin();
18209
18210 if ( empty( $params['sites'] ) ) {
18211 return;
18212 }
18213 }
18214
18215 if ( is_object( $bundle_license ) ) {
18216 $params['license_key'] = $bundle_license->secret_key;
18217 }
18218
18219 // Activate add-on with parent plugin credentials.
18220 $result = $parent_fs->get_api_site_scope()->call(
18221 "/addons/{$this->_plugin->id}/installs.json",
18222 'post',
18223 $params
18224 );
18225
18226 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18227 if ( is_object( $bundle_license ) ) {
18228 /**
18229 * When a license object is provided, it's an attempt by the SDK to activate a bundle license and not a user-initiated action, therefore, do not show any admin notice to avoid confusion (e.g.: the notice will show up just above the opt-in link). If the license activation fails, the admin will see an opt-in link instead.
18230 *
18231 * @author Leo Fajardo (@leorw)
18232 * @since 2.4.0
18233 */
18234 } else {
18235 $error_message = FS_Api::is_api_error_object( $result ) ?
18236 $result->error->message :
18237 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18238
18239 $this->_admin_notices->add(
18240 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18241 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18242 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18243 'error'
18244 );
18245 }
18246
18247 return;
18248 }
18249
18250 $addon_installs = $result->installs;
18251 foreach ( $addon_installs as $key => $addon_install ) {
18252 $addon_installs[ $key ] = new FS_Site( $addon_install );
18253 }
18254
18255 $first_install = $addon_installs[0];
18256
18257 // Get user information based on parent's plugin.
18258 $user = $parent_fs->get_user();
18259
18260 // First of all, set site and user info - otherwise we won't
18261 // be able to invoke API calls.
18262 $this->_site = $first_install;
18263 $this->_user = $user;
18264
18265 // Sync add-on plans.
18266 $this->_sync_plans();
18267
18268 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18269
18270 // Get site's current plan.
18271 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18272
18273 // Sync licenses.
18274 $this->_sync_licenses();
18275
18276 if ( ! fs_is_network_admin() ) {
18277 // Try to activate premium license.
18278 $this->_activate_license( true, $bundle_license );
18279
18280 if ( is_object( $bundle_license ) ) {
18281 $this->maybe_activate_bundle_license( $bundle_license );
18282 }
18283 } else {
18284 if ( is_object( $bundle_license ) ) {
18285 $premium_license = $bundle_license;
18286 } else {
18287 $license_id = fs_request_get( 'license_id' );
18288
18289 if ( is_object( $this->_site ) &&
18290 FS_Plugin_License::is_valid_id( $license_id ) &&
18291 $license_id == $this->_site->license_id
18292 ) {
18293 // License is already activated.
18294 return;
18295 }
18296
18297 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18298 $this->_get_license_by_id( $license_id ) :
18299 $this->_get_available_premium_license();
18300 }
18301
18302 if ( is_object( $premium_license ) ) {
18303 $this->maybe_network_activate_addon_license( $premium_license );
18304 }
18305 }
18306 }
18307
18308 /**
18309 * @author Leo Fajardo (@leorw)
18310 * @since 2.3.0
18311 *
18312 * @param FS_Site[] $installs
18313 * @param bool $is_site_level
18314 */
18315 private function handle_account_connection( $installs, $is_site_level ) {
18316 $first_install = $installs[0];
18317
18318 if ( $is_site_level ) {
18319 $this->_set_account( $this->_user, $first_install );
18320
18321 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18322 } else {
18323 $this->_store_user();
18324
18325 // Map site addresses to their blog IDs.
18326 $address_to_blog_map = $this->get_address_to_blog_map();
18327
18328 $first_blog_id = null;
18329 $blog_2_install_map = array();
18330 foreach ( $installs as $install ) {
18331 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18332 $blog_id = $address_to_blog_map[ $address ];
18333
18334 $this->_store_site( true, $blog_id, $install );
18335
18336 if ( is_null( $first_blog_id ) ) {
18337 $first_blog_id = $blog_id;
18338 }
18339
18340 $blog_2_install_map[ $blog_id ] = $install;
18341 }
18342
18343 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18344 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18345 ) {
18346 // Store network user.
18347 $this->_storage->network_user_id = $this->_user->id;
18348 }
18349
18350 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18351 $this->_storage->network_install_blog_id = $first_blog_id;
18352 }
18353
18354 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18355 // Super admin opted in for all sites in the network.
18356 $this->_storage->is_network_connected = true;
18357 }
18358
18359 $this->_store_licenses( false );
18360
18361 self::$_accounts->store();
18362
18363 // Don't sync the installs data on network upgrade
18364 if ( ! $this->network_upgrade_mode_completed() ) {
18365 $this->send_installs_update();
18366 }
18367
18368 $current_blog = get_current_blog_id();
18369
18370 foreach ( $blog_2_install_map as $blog_id => $install ) {
18371 $this->switch_to_blog( $blog_id );
18372
18373 $this->do_action( 'after_account_connection', $this->_user, $install );
18374 }
18375
18376 // Switch install context back to the first install.
18377 $this->switch_to_blog(
18378 $current_blog,
18379 $first_install,
18380 ( $this->_site->id != $first_install->id )
18381 );
18382
18383 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18384 }
18385 }
18386
18387 /**
18388 * Tries to activate parent account based on add-on's info.
18389 *
18390 * @author Vova Feldman (@svovaf)
18391 * @since 1.2.2.7
18392 *
18393 * @param Freemius $parent_fs
18394 */
18395 private function activate_parent_account( Freemius $parent_fs ) {
18396 if ( ! $this->is_addon() ) {
18397 // This is not an add-on.
18398 return;
18399 }
18400
18401 if ( $parent_fs->is_registered() ) {
18402 // Already activated.
18403 return;
18404 }
18405
18406 // Activate parent with add-on's user credentials.
18407 $parent_install = $this->get_api_user_scope()->call(
18408 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18409 'post',
18410 $parent_fs->get_install_data_for_api( array(
18411 'uid' => $parent_fs->get_anonymous_id(),
18412 ), false, false )
18413 );
18414
18415 if ( isset( $parent_install->error ) ) {
18416 $this->_admin_notices->add(
18417 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18418 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18419 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18420 'error'
18421 );
18422
18423 return;
18424 }
18425
18426 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18427
18428 if ( $parent_fs->is_pending_activation() ) {
18429 $parent_fs->clear_pending_activation_mode();
18430 }
18431
18432 // Get user information based on parent's plugin.
18433 $user = $this->get_user();
18434
18435 // First of all, set site info - otherwise we won't
18436 // be able to invoke API calls.
18437 $parent_fs->_site = new FS_Site( $parent_install );
18438 $parent_fs->_user = $user;
18439
18440 // Sync add-on plans.
18441 $parent_fs->_sync_plans();
18442
18443 $parent_fs->update_license_required_permissions_if_anonymous();
18444
18445 $parent_fs->_set_account( $user, $parent_fs->_site );
18446 }
18447
18448 #endregion
18449
18450 #----------------------------------------------------------------------------------
18451 #region Admin Menu Items
18452 #----------------------------------------------------------------------------------
18453
18454 private $_menu_items = array();
18455
18456 /**
18457 * @author Vova Feldman (@svovaf)
18458 * @since 1.2.1.8
18459 *
18460 * @return array
18461 */
18462 function get_menu_items() {
18463 return $this->_menu_items;
18464 }
18465
18466 /**
18467 * @author Vova Feldman (@svovaf)
18468 * @since 1.0.7
18469 *
18470 * @return string
18471 */
18472 function get_menu_slug() {
18473 return $this->_menu->get_slug();
18474 }
18475
18476 /**
18477 * @author Vova Feldman (@svovaf)
18478 * @since 1.0.9
18479 */
18480 function _prepare_admin_menu() {
18481 // if ( ! $this->is_on() ) {
18482 // return;
18483 // }
18484
18485 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18486 return;
18487 }
18488
18489 /**
18490 * When running from a site admin with a network activated module and the connection
18491 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18492 * site level settings.
18493 *
18494 * @author Vova Feldman (@svovaf)
18495 * @since 2.0.0
18496 */
18497 $should_hide_site_admin_settings = (
18498 $this->_is_network_active &&
18499 ! fs_is_network_admin() &&
18500 ! $this->is_delegated_connection() &&
18501 ! $this->is_anonymous() &&
18502 ! $this->is_registered()
18503 );
18504
18505 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18506
18507 if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18508 $should_hide_site_admin_settings
18509 ) {
18510 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18511 } else {
18512 $this->do_action( fs_is_network_admin() ?
18513 'before_network_admin_menu_init' :
18514 'before_admin_menu_init'
18515 );
18516
18517 $this->add_menu_action();
18518
18519 $this->add_network_menu_when_missing();
18520
18521 $this->add_submenu_items();
18522 }
18523 }
18524
18525 /**
18526 * Admin dashboard menu items modifications.
18527 *
18528 * NOTE: admin_menu action executed before admin_init.
18529 *
18530 * @author Vova Feldman (@svovaf)
18531 * @since 1.0.7
18532 *
18533 */
18534 private function add_menu_action() {
18535 if ( $this->is_activation_mode() ) {
18536 if ( $this->show_opt_in_on_setting_page() ) {
18537 $this->override_plugin_menu_with_activation();
18538 } else {
18539 /**
18540 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18541 */
18542 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18543 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18544 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18545 fs_request_get_bool( 'pending_activation' )
18546 ) {
18547 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18548 }
18549 }
18550 } else {
18551 if ( ! $this->is_registered() ) {
18552 // If not registered try to install user.
18553 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18554 $this->_install_with_new_user();
18555 }
18556 } else if (
18557 fs_request_is_action( 'sync_user' ) &&
18558 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18559 ) {
18560 $this->_handle_account_user_sync();
18561 }
18562 }
18563 }
18564
18565 /**
18566 * @author Vova Feldman (@svovaf)
18567 * @since 1.0.1
18568 */
18569 function _redirect_on_clicked_menu_link() {
18570 $this->_logger->entrance();
18571
18572 $page = fs_request_get('page');
18573 $page = is_string($page) ? strtolower($page) : '';
18574
18575 $this->_logger->log( 'page = ' . $page );
18576
18577 foreach ( $this->_menu_items as $priority => $items ) {
18578 foreach ( $items as $item ) {
18579 if ( isset( $item['url'] ) ) {
18580 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18581 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18582
18583 fs_redirect( $item['url'] );
18584 }
18585 }
18586 }
18587 }
18588 }
18589
18590 /**
18591 * Remove plugin's all admin menu items & pages, and replace with activation page.
18592 *
18593 * @author Vova Feldman (@svovaf)
18594 * @since 1.0.1
18595 */
18596 private function override_plugin_menu_with_activation() {
18597 $this->_logger->entrance();
18598
18599 $hook = false;
18600
18601 if ( ! $this->has_settings_menu() ) {
18602 // Add the opt-in page without a menu item.
18603 $hook = FS_Admin_Menu_Manager::add_subpage(
18604 '',
18605 $this->get_plugin_name(),
18606 $this->get_plugin_name(),
18607 'manage_options',
18608 $this->_slug,
18609 array( &$this, '_connect_page_render' )
18610 );
18611 } else if ( $this->_menu->is_top_level() ) {
18612 if ( $this->_menu->is_override_exact() ) {
18613 // Make sure the current page is matching the activation page.
18614 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18615 return;
18616 }
18617 }
18618
18619 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18620
18621 if ( false === $hook ) {
18622 // Create new menu item just for the opt-in.
18623 $hook = FS_Admin_Menu_Manager::add_page(
18624 $this->get_plugin_name(),
18625 $this->get_plugin_name(),
18626 'manage_options',
18627 $this->_menu->get_slug(),
18628 array( &$this, '_connect_page_render' )
18629 );
18630 }
18631 } else {
18632 $menus = array( $this->_menu->get_parent_slug() );
18633
18634 if ( $this->_menu->is_override_exact() ) {
18635 // Make sure the current page is matching the activation page.
18636 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18637 return;
18638 }
18639 }
18640
18641 foreach ( $menus as $parent_slug ) {
18642 $hook = $this->_menu->override_submenu_action(
18643 $parent_slug,
18644 $this->_menu->get_raw_slug(),
18645 array( &$this, '_connect_page_render' )
18646 );
18647
18648 if ( false !== $hook ) {
18649 // Found plugin's submenu item.
18650 break;
18651 }
18652 }
18653 }
18654
18655 if ( $this->is_activation_page() ) {
18656 // Clean admin page from distracting content.
18657 self::_clean_admin_content_section();
18658 }
18659
18660 if ( false !== $hook ) {
18661 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18662 $this->_install_with_current_user();
18663 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18664 $this->_install_with_new_user();
18665 }
18666 }
18667 }
18668
18669 /**
18670 * If a plugin was network activated and connected but don't have a network
18671 * level settings, then add an artificial menu item for the Account and other
18672 * Freemius settings.
18673 *
18674 * @author Vova Feldman (@svovaf)
18675 * @since 2.0.0
18676 */
18677 private function add_network_menu_when_missing() {
18678 $this->_logger->entrance();
18679
18680 if ( ! $this->_is_network_active ) {
18681 // Plugin wasn't activated on the network level.
18682 return;
18683 }
18684
18685 if ( ! fs_is_network_admin() ) {
18686 // The context is not the network admin.
18687 return;
18688 }
18689
18690 if ( $this->_menu->has_network_menu() ) {
18691 // Plugin already has a network level menu.
18692 return;
18693 }
18694
18695 if ( $this->is_network_activation_mode() ) {
18696 /**
18697 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
18698 * screen is being shown.
18699 *
18700 * @author Leo Fajardo (@leorw)
18701 */
18702 return;
18703 }
18704
18705 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
18706 if ( $this->is_network_delegated_connection() ) {
18707 // Super-admin delegated the connection to the site admins.
18708 return;
18709 }
18710 }
18711
18712 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
18713
18714 if ( $this->_menu->has_menu() ||
18715 ! $this->is_addon() ||
18716 $this->is_activation_mode()
18717 ) {
18718 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
18719 $this->get_plugin_name(),
18720 $this->get_plugin_name(),
18721 'manage_options',
18722 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
18723 );
18724 }
18725 } else {
18726 $this->_menu->add_subpage_and_update(
18727 $this->_menu->get_parent_slug(),
18728 $this->get_plugin_name(),
18729 $this->get_plugin_name(),
18730 'manage_options',
18731 $this->_menu->get_slug()
18732 );
18733 }
18734 }
18735
18736 /**
18737 * @author Leo Fajardo (@leorw)
18738 * @since 1.2.1
18739 *
18740 * return string
18741 */
18742 function get_top_level_menu_capability() {
18743 global $menu;
18744
18745 $top_level_menu_slug = $this->get_top_level_menu_slug();
18746
18747 foreach ( $menu as $menu_info ) {
18748 /**
18749 * The second element in the menu info array is the capability/role that has access to the menu and the
18750 * third element is the menu slug.
18751 */
18752 if ( $menu_info[2] === $top_level_menu_slug ) {
18753 return $menu_info[1];
18754 }
18755 }
18756
18757 return 'read';
18758 }
18759
18760 /**
18761 * @author Vova Feldman (@svovaf)
18762 * @since 1.0.0
18763 *
18764 * @return string
18765 */
18766 private function get_top_level_menu_slug() {
18767 return ( $this->is_addon() ?
18768 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
18769 $this->_menu->get_top_level_menu_slug() );
18770 }
18771
18772 /**
18773 * @author Vova Feldman (@svovaf)
18774 * @since 1.2.2.7
18775 *
18776 * @return string
18777 */
18778 function get_pricing_cta_label() {
18779 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
18780
18781 if ( $this->is_in_trial_promotion() &&
18782 ! $this->is_paying_or_trial()
18783 ) {
18784 // If running a trial promotion, modify the pricing to load the trial.
18785 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
18786 } else if ( $this->is_paying() ) {
18787 $label = $this->get_text_inline( 'Pricing', 'pricing' );
18788 }
18789
18790 return $label;
18791 }
18792
18793 /**
18794 * @author Vova Feldman (@svovaf)
18795 * @since 1.2.2.7
18796 *
18797 * @return bool
18798 */
18799 function is_pricing_page_visible() {
18800 $visible = (
18801 // Has at least one paid plan.
18802 $this->has_paid_plan() &&
18803 // Didn't ask to hide the pricing page.
18804 $this->is_page_visible( 'pricing' ) &&
18805 // Don't have a valid active license or has more than one plan.
18806 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
18807 );
18808
18809 return $this->apply_filters( 'is_pricing_page_visible', $visible );
18810 }
18811
18812 /**
18813 * @author Leo Fajardo (@leorw)
18814 * @since 2.3.0
18815 *
18816 * @param bool $is_activation_mode
18817 *
18818 * @return bool
18819 */
18820 private function should_add_submenu_or_action_links( $is_activation_mode ) {
18821 if ( $this->is_addon() ) {
18822 // No submenu items or action links for add-ons.
18823 return false;
18824 }
18825
18826 if ( $this->show_opt_in_on_themes_page() ) {
18827 if ( ! fs_is_network_admin() ) {
18828 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
18829 return true;
18830 }
18831 } else if ( $is_activation_mode ) {
18832 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
18833 return false;
18834 }
18835
18836 if ( fs_is_network_admin() ) {
18837 /**
18838 * Add submenu items or action links to network level when plugin was network activated and the super
18839 * admin did NOT delegate the connection of all sites to site admins.
18840 */
18841 return (
18842 $this->_is_network_active &&
18843 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
18844 ! $this->is_network_delegated_connection() )
18845 );
18846 }
18847
18848 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
18849 }
18850
18851 /**
18852 * Add default Freemius menu items.
18853 *
18854 * @author Vova Feldman (@svovaf)
18855 * @since 1.0.0
18856 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
18857 */
18858 private function add_submenu_items() {
18859 $this->_logger->entrance();
18860
18861 $is_activation_mode = $this->is_activation_mode();
18862
18863 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
18864
18865 if ( $add_submenu_items ) {
18866 if ( $this->has_affiliate_program() ) {
18867 // Add affiliation page.
18868 $this->add_submenu_item(
18869 $this->get_text_inline( 'Affiliation', 'affiliation' ),
18870 array( &$this, '_affiliation_page_render' ),
18871 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
18872 'manage_options',
18873 'affiliation',
18874 'Freemius::_clean_admin_content_section',
18875 WP_FS__DEFAULT_PRIORITY,
18876 $this->is_submenu_item_visible( 'affiliation' )
18877 );
18878 }
18879 }
18880
18881 if ( $add_submenu_items ||
18882 ( $is_activation_mode &&
18883 $this->is_only_premium() &&
18884 $this->is_admin_page( 'account' ) &&
18885 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
18886 )
18887 ) {
18888 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
18889 $show_account = (
18890 $this->is_submenu_item_visible( 'account' ) &&
18891 /**
18892 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
18893 */
18894 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
18895 );
18896
18897 // Add user account page.
18898 $this->add_submenu_item(
18899 $this->get_text_inline( 'Account', 'account' ),
18900 array( &$this, '_account_page_render' ),
18901 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
18902 'manage_options',
18903 'account',
18904 array( &$this, '_account_page_load' ),
18905 WP_FS__DEFAULT_PRIORITY,
18906 ( $add_submenu_items && $show_account )
18907 );
18908 }
18909 }
18910
18911 if ( $add_submenu_items ) {
18912 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18913 // Add contact page.
18914 if ( $this->is_premium() ) {
18915 $this->add_submenu_item(
18916 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18917 array( &$this, '_contact_page_render' ),
18918 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
18919 'manage_options',
18920 'contact',
18921 'Freemius::_clean_admin_content_section',
18922 WP_FS__DEFAULT_PRIORITY,
18923 $this->is_submenu_item_visible( 'contact' )
18924 );
18925 } else {
18926 $this->add_submenu_link_item(
18927 $this->get_text_inline( 'Contact Us', 'contact-us' ),
18928 FS_Contact_Form_Manager::instance()->get_standalone_link( $this ),
18929 'contact',
18930 'manage_options',
18931 WP_FS__DEFAULT_PRIORITY,
18932 $this->is_submenu_item_visible( 'contact' ),
18933 'fs_external_contact',
18934 true
18935 );
18936 }
18937 }
18938
18939 if ( $this->has_addons() ) {
18940 $this->add_submenu_item(
18941 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18942 array( &$this, '_addons_page_render' ),
18943 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18944 'manage_options',
18945 'addons',
18946 array( &$this, '_addons_page_load' ),
18947 WP_FS__LOWEST_PRIORITY - 1,
18948 $this->is_submenu_item_visible( 'addons' )
18949 );
18950 }
18951 }
18952
18953 if ( $add_submenu_items ||
18954 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
18955 ) {
18956 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
18957 $show_pricing = (
18958 $this->is_submenu_item_visible( 'pricing' ) &&
18959 $this->is_pricing_page_visible()
18960 );
18961
18962 $pricing_cta_text = $this->get_pricing_cta_label();
18963 $pricing_class = 'upgrade-mode';
18964 if ( $show_pricing ) {
18965 if ( $this->is_in_trial_promotion() &&
18966 ! $this->is_paying_or_trial()
18967 ) {
18968 // If running a trial promotion, modify the pricing to load the trial.
18969 $pricing_class = 'trial-mode';
18970 } else if ( $this->is_paying() ) {
18971 $pricing_class = '';
18972 }
18973 }
18974
18975 $custom_pricing_url = $this->get_pricing_url_with_filter( null );
18976 $pricing_menu_title = $pricing_cta_text . '&nbsp;&nbsp;' . ( is_rtl() ? $this->get_text_x_inline( '&#x2190;', 'ASCII arrow left icon', 'symbol_arrow-left' ) : $this->get_text_x_inline( '&#x27a4;', 'ASCII arrow right icon', 'symbol_arrow-right' ) );
18977 $show_pricing_submenu_item = ( $add_submenu_items && $show_pricing );
18978
18979 // Add upgrade/pricing submenu item.
18980 if ( ! is_null( $custom_pricing_url ) ) {
18981 $this->add_submenu_link_item(
18982 $pricing_menu_title,
18983 $custom_pricing_url,
18984 'pricing',
18985 'manage_options',
18986 WP_FS__LOWEST_PRIORITY,
18987 $show_pricing_submenu_item,
18988 $pricing_class
18989 );
18990 } else {
18991 $this->add_submenu_item(
18992 $pricing_menu_title,
18993 array( &$this, '_pricing_page_render' ),
18994 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
18995 'manage_options',
18996 'pricing',
18997 'Freemius::_clean_admin_content_section',
18998 WP_FS__LOWEST_PRIORITY,
18999 $show_pricing_submenu_item,
19000 $pricing_class
19001 );
19002 }
19003 }
19004 }
19005
19006 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19007 /**
19008 * Add the other menu items if there are any when not in activation mode or license activation is not
19009 * required (license activation is required for registered or anonymous users after activating the
19010 * premium version when the site is not in trial mode or there's no active valid license).
19011 *
19012 * @author Leo Fajardo (@leorw)
19013 * @since 2.2.1
19014 */
19015 if ( 0 < count( $this->_menu_items ) ) {
19016 if ( ! $this->_menu->is_top_level() ) {
19017 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19018
19019 // Append submenu items right after the plugin's submenu item.
19020 $this->order_sub_submenu_items();
19021 } else {
19022 // Append submenu items.
19023 $this->embed_submenu_items();
19024 }
19025 }
19026 }
19027 }
19028
19029 /**
19030 * Moved the actual submenu item additions to a separated function,
19031 * in order to support sub-submenu items when the plugin's settings
19032 * only have a submenu and not top-level menu item.
19033 *
19034 * @author Vova Feldman (@svovaf)
19035 * @since 1.1.4
19036 */
19037 private function embed_submenu_items() {
19038 $item_classes = $this->_menu->is_top_level() ? 'fs-submenu-item' : 'fs-submenu-item fs-sub';
19039
19040 $item_template = '<span class="' . $item_classes . ' %1$s %2$s %3$s" data-fs-external-url="%5$s" data-fs-new-tab="%6$s">%4$s</span>';
19041
19042 $top_level_menu_capability = $this->get_top_level_menu_capability();
19043
19044 ksort( $this->_menu_items );
19045
19046 $is_first_submenu_item = true;
19047
19048 foreach ( $this->_menu_items as $priority => $items ) {
19049 foreach ( $items as $item ) {
19050 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19051
19052 $menu_item = sprintf(
19053 $item_template,
19054 $this->get_unique_affix(),
19055 $item['menu_slug'],
19056 ! empty( $item['class'] ) ? $item['class'] : '',
19057 $item['menu_title'],
19058 esc_attr( isset( $item['url'] ) ? $item['url'] : '' ),
19059 esc_attr( isset( $item['new_tab'] ) ? 'true' : 'false' )
19060 );
19061
19062 $top_level_menu_slug = $this->get_top_level_menu_slug();
19063 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19064
19065 if ( ! isset( $item['url'] ) ) {
19066 $hook = FS_Admin_Menu_Manager::add_subpage(
19067 $item['show_submenu'] ?
19068 $top_level_menu_slug :
19069 '',
19070 $item['page_title'],
19071 $menu_item,
19072 $capability,
19073 $menu_slug,
19074 $item['render_function']
19075 );
19076
19077 if ( false !== $item['before_render_function'] ) {
19078 add_action( "load-$hook", $item['before_render_function'] );
19079 }
19080 } else {
19081 FS_Admin_Menu_Manager::add_subpage(
19082 $item['show_submenu'] ?
19083 $top_level_menu_slug :
19084 '',
19085 $item['page_title'],
19086 $menu_item,
19087 $capability,
19088 $menu_slug,
19089 array( $this, '' )
19090 );
19091 }
19092
19093 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19094 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19095 /**
19096 * If the top-level menu has been dynamically created, remove the first submenu item that
19097 * WordPress automatically creates when there's no submenu item whose slug matches the
19098 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19099 *
19100 * Awesome Plugin
19101 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19102 *
19103 * @author Leo Fajardo (@leorw)
19104 */
19105 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19106 }
19107
19108 $is_first_submenu_item = false;
19109 }
19110 }
19111 }
19112 }
19113
19114 /**
19115 * Re-order the submenu items so all Freemius added new submenu items
19116 * are added right after the plugin's settings submenu item.
19117 *
19118 * @author Vova Feldman (@svovaf)
19119 * @since 1.1.4
19120 */
19121 private function order_sub_submenu_items() {
19122 global $submenu;
19123
19124 $menu_slug = $this->_menu->get_top_level_menu_slug();
19125
19126 /**
19127 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19128 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19129 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19130 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19131 *
19132 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19133 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19134 * capability needed to access the parent menu as the capability for the submenus that we will add.
19135 */
19136 if ( empty( $submenu[ $menu_slug ] ) ) {
19137 return;
19138 }
19139
19140 $top_level_menu = &$submenu[ $menu_slug ];
19141
19142 $all_submenu_items_after = array();
19143
19144 $found_submenu_item = false;
19145
19146 foreach ( $top_level_menu as $submenu_id => $meta ) {
19147 if ( $found_submenu_item ) {
19148 // Remove all submenu items after the plugin's submenu item.
19149 $all_submenu_items_after[] = $meta;
19150 unset( $top_level_menu[ $submenu_id ] );
19151 }
19152
19153 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19154 // Found the submenu item, put all below.
19155 $found_submenu_item = true;
19156 continue;
19157 }
19158 }
19159
19160 // Embed all plugin's new submenu items.
19161 $this->embed_submenu_items();
19162
19163 // Start with specially high number to make sure it's appended.
19164 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19165 foreach ( $all_submenu_items_after as $meta ) {
19166 $top_level_menu[ $i ] = $meta;
19167 $i ++;
19168 }
19169
19170 // Sort submenu items.
19171 ksort( $top_level_menu );
19172 }
19173
19174 /**
19175 * Helper method to return the module's support forum URL.
19176 *
19177 * @author Vova Feldman (@svovaf)
19178 * @since 1.2.2.7
19179 *
19180 * @return string
19181 */
19182 function get_support_forum_url() {
19183 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19184 }
19185
19186 /**
19187 * Displays the Support Forum link when enabled.
19188 *
19189 * Can be filtered like so:
19190 *
19191 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19192 * if ( 'support' === $menu_id ) {
19193 * return _fs->is_registered();
19194 * }
19195 * return $is_visible;
19196 * }
19197 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19198 *
19199 */
19200 function _add_default_submenu_items() {
19201 if ( ! $this->is_on() ) {
19202 return;
19203 }
19204
19205 if ( ! $this->is_activation_mode() &&
19206 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19207 ( ! $this->_is_network_active && is_admin() ) )
19208 ) {
19209 $this->add_submenu_link_item(
19210 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19211 $this->get_support_forum_url(),
19212 'wp-support-forum',
19213 null,
19214 50,
19215 $this->is_submenu_item_visible( 'support' ),
19216 '',
19217 true
19218 );
19219 }
19220 }
19221
19222 /**
19223 * @author Vova Feldman (@svovaf)
19224 * @since 1.0.1
19225 *
19226 * @param string $menu_title
19227 * @param callable $render_function
19228 * @param bool|string $page_title
19229 * @param string $capability
19230 * @param bool|string $menu_slug
19231 * @param bool|callable $before_render_function
19232 * @param int $priority
19233 * @param bool $show_submenu
19234 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19235 */
19236 function add_submenu_item(
19237 $menu_title,
19238 $render_function,
19239 $page_title = false,
19240 $capability = 'manage_options',
19241 $menu_slug = false,
19242 $before_render_function = false,
19243 $priority = WP_FS__DEFAULT_PRIORITY,
19244 $show_submenu = true,
19245 $class = ''
19246 ) {
19247 $this->_logger->entrance( 'Title = ' . $menu_title );
19248
19249 if ( $this->is_addon() ) {
19250 $parent_fs = $this->get_parent_instance();
19251
19252 if ( is_object( $parent_fs ) ) {
19253 $parent_fs->add_submenu_item(
19254 $menu_title,
19255 $render_function,
19256 $page_title,
19257 $capability,
19258 $menu_slug,
19259 $before_render_function,
19260 $priority,
19261 $show_submenu,
19262 $class
19263 );
19264
19265 return;
19266 }
19267 }
19268
19269 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19270 $this->_menu_items[ $priority ] = array();
19271 }
19272
19273 $this->_menu_items[ $priority ][] = array(
19274 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19275 'menu_title' => $menu_title,
19276 'capability' => $capability,
19277 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19278 'render_function' => $render_function,
19279 'before_render_function' => $before_render_function,
19280 'show_submenu' => $show_submenu,
19281 'class' => $class,
19282 );
19283 }
19284
19285 /**
19286 * @author Vova Feldman (@svovaf)
19287 * @since 1.0.1
19288 *
19289 * @param string $menu_title
19290 * @param string $url
19291 * @param bool $menu_slug
19292 * @param string $capability
19293 * @param int $priority
19294 * @param bool $show_submenu
19295 * @param string $class
19296 * @param bool $new_tab
19297 */
19298 function add_submenu_link_item(
19299 $menu_title,
19300 $url,
19301 $menu_slug = false,
19302 $capability = 'read',
19303 $priority = WP_FS__DEFAULT_PRIORITY,
19304 $show_submenu = true,
19305 $class = '',
19306 $new_tab = false
19307 ) {
19308 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19309
19310 if ( $this->is_addon() ) {
19311 $parent_fs = $this->get_parent_instance();
19312
19313 if ( is_object( $parent_fs ) ) {
19314 $parent_fs->add_submenu_link_item(
19315 $menu_title,
19316 $url,
19317 $menu_slug,
19318 $capability,
19319 $priority,
19320 $show_submenu,
19321 $class,
19322 $new_tab
19323 );
19324
19325 return;
19326 }
19327 }
19328
19329 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19330 $this->_menu_items[ $priority ] = array();
19331 }
19332
19333 $this->_menu_items[ $priority ][] = array(
19334 'menu_title' => $menu_title,
19335 'capability' => $capability,
19336 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19337 'url' => $url,
19338 'page_title' => $menu_title,
19339 'render_function' => 'fs_dummy',
19340 'before_render_function' => '',
19341 'show_submenu' => $show_submenu,
19342 'class' => $class,
19343 'new_tab' => $new_tab,
19344 );
19345 }
19346
19347 #endregion ------------------------------------------------------------------
19348
19349 #--------------------------------------------------------------------------------
19350 #region Admin Notices
19351 #--------------------------------------------------------------------------------
19352
19353 /**
19354 * @author Vova Feldman (@svovaf)
19355 * @since 2.3.1
19356 *
19357 * @param string|string[] $ids
19358 * @param int|null $network_level_or_blog_id
19359 *
19360 * @uses FS_Admin_Notices::remove_sticky()
19361 */
19362 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19363 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19364 }
19365
19366 #endregion
19367
19368 #--------------------------------------------------------------------------------
19369 #region Actions / Hooks / Filters
19370 #--------------------------------------------------------------------------------
19371
19372 /**
19373 * @author Vova Feldman (@svovaf)
19374 * @since 1.1.7
19375 *
19376 * @param string $tag
19377 *
19378 * @return string
19379 */
19380 public function get_action_tag( $tag ) {
19381 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19382 }
19383
19384 /**
19385 * @author Vova Feldman (@svovaf)
19386 * @since 1.2.1.6
19387 *
19388 * @param string $tag
19389 * @param string $slug
19390 * @param bool $is_plugin
19391 *
19392 * @return string
19393 */
19394 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19395 $action = "fs_{$tag}";
19396
19397 if ( ! empty( $slug ) ) {
19398 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19399 }
19400
19401 return $action;
19402 }
19403
19404 /**
19405 * Returns a string that can be used to generate a unique action name,
19406 * option name, HTML element ID, or HTML element class.
19407 *
19408 * @author Leo Fajardo (@leorw)
19409 * @since 1.2.2
19410 *
19411 * @return string
19412 */
19413 public function get_unique_affix() {
19414 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19415 }
19416
19417 /**
19418 * Returns a string that can be used to generate a unique action name,
19419 * option name, HTML element ID, or HTML element class.
19420 *
19421 * @author Vova Feldman (@svovaf)
19422 * @since 1.2.2.5
19423 *
19424 * @param string $slug
19425 * @param bool $is_plugin
19426 *
19427 * @return string
19428 */
19429 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19430 $affix = $slug;
19431
19432 if ( ! $is_plugin ) {
19433 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19434 }
19435
19436 return $affix;
19437 }
19438
19439 /**
19440 * @author Vova Feldman (@svovaf)
19441 * @since 1.2.1
19442 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19443 * based on the slug for backward compatibility.
19444 *
19445 * @param string $tag
19446 *
19447 * @return string
19448 */
19449 function get_ajax_action( $tag ) {
19450 return self::get_ajax_action_static( $tag, $this->_module_id );
19451 }
19452
19453 /**
19454 * @author Vova Feldman (@svovaf)
19455 * @since 1.2.1.7
19456 *
19457 * @param string $tag
19458 *
19459 * @return string
19460 */
19461 function get_ajax_security( $tag ) {
19462 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19463 }
19464
19465 /**
19466 * @author Vova Feldman (@svovaf)
19467 * @since 1.2.1.7
19468 *
19469 * @param string $tag
19470 */
19471 function check_ajax_referer( $tag ) {
19472 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19473 }
19474
19475 /**
19476 * @author Vova Feldman (@svovaf)
19477 * @since 1.2.1.6
19478 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19479 * based on the slug for backward compatibility.
19480 *
19481 * @param string $tag
19482 * @param number|null $module_id
19483 *
19484 * @return string
19485 */
19486 static function get_ajax_action_static( $tag, $module_id = null ) {
19487 $action = "fs_{$tag}";
19488
19489 if ( ! empty( $module_id ) ) {
19490 $action .= "_{$module_id}";
19491 }
19492
19493 return $action;
19494 }
19495
19496 /**
19497 * Do action, specific for the current context plugin.
19498 *
19499 * @author Vova Feldman (@svovaf)
19500 * @since 1.0.1
19501 *
19502 * @param string $tag The name of the action to be executed.
19503 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19504 * functions hooked to the action. Default empty.
19505 *
19506 * @uses do_action()
19507 */
19508 function do_action( $tag, $arg = '' ) {
19509 $args = func_get_args();
19510
19511 $this->_logger->entrance( $tag );
19512
19513 call_user_func_array( 'do_action', array_merge(
19514 array( $this->get_action_tag( $tag ) ),
19515 array_slice( $args, 1 ) )
19516 );
19517 }
19518
19519 /**
19520 * Add action, specific for the current context plugin.
19521 *
19522 * @author Vova Feldman (@svovaf)
19523 * @since 1.0.1
19524 *
19525 * @param string $tag
19526 * @param callable $function_to_add
19527 * @param int $priority
19528 * @param int $accepted_args
19529 *
19530 * @uses add_action()
19531 */
19532 function add_action(
19533 $tag,
19534 $function_to_add,
19535 $priority = WP_FS__DEFAULT_PRIORITY,
19536 $accepted_args = 1
19537 ) {
19538 $this->_logger->entrance( $tag );
19539
19540 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19541 }
19542
19543 /**
19544 * Add AJAX action, specific for the current context plugin.
19545 *
19546 * @author Vova Feldman (@svovaf)
19547 * @since 1.2.1
19548 *
19549 * @param string $tag
19550 * @param callable $function_to_add
19551 * @param int $priority
19552 *
19553 * @uses add_action()
19554 *
19555 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19556 */
19557 function add_ajax_action(
19558 $tag,
19559 $function_to_add,
19560 $priority = WP_FS__DEFAULT_PRIORITY
19561 ) {
19562 $this->_logger->entrance( $tag );
19563
19564 return self::add_ajax_action_static(
19565 $tag,
19566 $function_to_add,
19567 $priority,
19568 $this->_module_id
19569 );
19570 }
19571
19572 /**
19573 * Add AJAX action.
19574 *
19575 * @author Vova Feldman (@svovaf)
19576 * @since 1.2.1.6
19577 *
19578 * @param string $tag
19579 * @param callable $function_to_add
19580 * @param int $priority
19581 * @param number|null $module_id
19582 *
19583 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19584 * @uses add_action()
19585 *
19586 */
19587 static function add_ajax_action_static(
19588 $tag,
19589 $function_to_add,
19590 $priority = WP_FS__DEFAULT_PRIORITY,
19591 $module_id = null
19592 ) {
19593 self::$_static_logger->entrance( $tag );
19594
19595 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19596 return false;
19597 }
19598
19599 add_action(
19600 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19601 $function_to_add,
19602 $priority,
19603 0
19604 );
19605
19606 self::$_static_logger->info( "$tag AJAX callback action added." );
19607
19608 return true;
19609 }
19610
19611 /**
19612 * Send a JSON response back to an Ajax request.
19613 *
19614 * @author Vova Feldman (@svovaf)
19615 * @since 1.2.1.5
19616 *
19617 * @param mixed $response
19618 */
19619 static function shoot_ajax_response( $response ) {
19620 wp_send_json( $response );
19621 }
19622
19623 /**
19624 * Send a JSON response back to an Ajax request, indicating success.
19625 *
19626 * @author Vova Feldman (@svovaf)
19627 * @since 1.2.1.5
19628 *
19629 * @param mixed $data Data to encode as JSON, then print and exit.
19630 */
19631 static function shoot_ajax_success( $data = null ) {
19632 wp_send_json_success( $data );
19633 }
19634
19635 /**
19636 * Send a JSON response back to an Ajax request, indicating failure.
19637 *
19638 * @author Vova Feldman (@svovaf)
19639 * @since 1.2.1.5
19640 *
19641 * @param mixed $error Optional error message.
19642 */
19643 static function shoot_ajax_failure( $error = '' ) {
19644 $result = array( 'success' => false );
19645 if ( ! empty( $error ) ) {
19646 $result['error'] = $error;
19647 }
19648
19649 wp_send_json( $result );
19650 }
19651
19652 /**
19653 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19654 *
19655 * @author Vova Feldman (@svovaf)
19656 * @since 2.5.1
19657 *
19658 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19659 *
19660 * @return string
19661 */
19662 static function ajax_url( $wrap_with = "'") {
19663 if ( fs_is_network_admin() ) {
19664 $param_name = '_fs_network_admin';
19665 } else {
19666 $param_name = '_fs_blog_admin';
19667 }
19668
19669 $url = admin_url( 'admin-ajax.php', 'relative' );
19670 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
19671 $url .= "{$param_name}=true";
19672
19673 return "{$wrap_with}{$url}{$wrap_with}";
19674 }
19675
19676 /**
19677 * Apply filter, specific for the current context plugin.
19678 *
19679 * @author Vova Feldman (@svovaf)
19680 * @since 1.0.9
19681 *
19682 * @param string $tag The name of the filter hook.
19683 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
19684 *
19685 * @return mixed The filtered value after all hooked functions are applied to it.
19686 *
19687 * @uses apply_filters()
19688 */
19689 function apply_filters( $tag, $value ) {
19690 $args = func_get_args();
19691
19692 $this->_logger->entrance( $tag );
19693
19694 array_unshift( $args, $this->get_unique_affix() );
19695
19696 return call_user_func_array( 'fs_apply_filter', $args );
19697 }
19698
19699 /**
19700 * Add filter, specific for the current context plugin.
19701 *
19702 * @author Vova Feldman (@svovaf)
19703 * @since 1.0.9
19704 *
19705 * @param string $tag
19706 * @param callable $function_to_add
19707 * @param int $priority
19708 * @param int $accepted_args
19709 *
19710 * @uses add_filter()
19711 */
19712 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
19713 $this->_logger->entrance( $tag );
19714
19715 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19716 }
19717
19718 /**
19719 * Check if has filter.
19720 *
19721 * @author Vova Feldman (@svovaf)
19722 * @since 1.1.4
19723 *
19724 * @param string $tag
19725 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
19726 *
19727 * @return false|int
19728 *
19729 * @uses has_filter()
19730 */
19731 function has_filter( $tag, $function_to_check = false ) {
19732 $this->_logger->entrance( $tag );
19733
19734 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
19735 }
19736
19737 #endregion
19738
19739 /**
19740 * Override default i18n text phrases.
19741 *
19742 * @author Vova Feldman (@svovaf)
19743 * @since 1.1.6
19744 *
19745 * @param string[] string $key_value
19746 *
19747 * @uses fs_override_i18n()
19748 */
19749 function override_i18n( $key_value ) {
19750 fs_override_i18n( $key_value, $this->_slug );
19751 }
19752
19753 /* Account Page
19754 ------------------------------------------------------------------------------------------------------------------*/
19755 /**
19756 * Update site information.
19757 *
19758 * @author Vova Feldman (@svovaf)
19759 * @since 1.0.1
19760 *
19761 * @param bool $store Flush to Database if true.
19762 * @param null|int $network_level_or_blog_id Since 2.0.0
19763 * @param \FS_Site $site Since 2.0.0
19764 */
19765 private function _store_site( $store = true, $network_level_or_blog_id = null, $site = null, $is_backup = false ) {
19766 $this->_logger->entrance();
19767
19768 if ( is_null( $site ) ) {
19769 $site = $this->_site;
19770 }
19771
19772 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
19773 $this->_logger->error( "Empty install ID, can't store site." );
19774
19775 return;
19776 }
19777
19778 $site_clone = clone $site;
19779
19780 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
19781
19782 if (
19783 ! $is_backup &&
19784 is_object( $this->_user ) && $this->_user->id != $site->user_id
19785 ) {
19786 $this->sync_user_by_current_install( $site->user_id );
19787
19788 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
19789
19790 if ( empty( $prev_stored_user_id ) &&
19791 is_object($this->_user) && $this->_user->id != $site->user_id
19792 ) {
19793 /**
19794 * Store the current user ID as the previous user ID so that the previous user can be used
19795 * as the install's owner while the new owner's details are not yet available.
19796 *
19797 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
19798 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
19799 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
19800 * into `replica`.
19801 *
19802 * @author Leo Fajardo (@leorw)
19803 */
19804 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
19805 }
19806 }
19807
19808 $sites[ $this->_slug ] = $site_clone;
19809
19810 $this->set_account_option(
19811 ( $is_backup ? 'prev_' : '' ) . 'sites',
19812 $sites,
19813 $store,
19814 $network_level_or_blog_id
19815 );
19816 }
19817
19818 /**
19819 * Stores the context site in the sites backup storage. This logic is used before deleting the site info so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails).
19820 *
19821 * @author Leo Fajardo (@leorw)
19822 * @since 2.5.0
19823 */
19824 private function back_up_site() {
19825 $this->_logger->entrance();
19826
19827 $site_clone = clone $this->_site;
19828
19829 $this->_store_site( true, null, $site_clone, true );
19830 }
19831
19832 /**
19833 * Update plugin's plans information.
19834 *
19835 * @author Vova Feldman (@svovaf)
19836 * @since 1.0.2
19837 *
19838 * @param bool $store Flush to Database if true.
19839 */
19840 private function _store_plans( $store = true ) {
19841 $this->_logger->entrance();
19842
19843 $plans = self::get_all_plans( $this->_module_type );
19844
19845 // Copy plans.
19846 $encrypted_plans = array();
19847 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
19848 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
19849 }
19850
19851 $plans[ $this->_slug ] = $encrypted_plans;
19852
19853 $this->set_account_option( 'plans', $plans, $store );
19854 }
19855
19856 /**
19857 * Update user's plugin licenses.
19858 *
19859 * @author Vova Feldman (@svovaf)
19860 * @since 1.0.5
19861 *
19862 * @param bool $store
19863 * @param number|bool $module_id
19864 * @param FS_Plugin_License[] $licenses
19865 */
19866 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
19867 $this->_logger->entrance();
19868
19869 $all_licenses = self::get_all_licenses();
19870
19871 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
19872 $module_id = $this->_module_id;
19873
19874 $user_licenses = is_array( $this->_licenses ) ?
19875 $this->_licenses :
19876 array();
19877
19878 if ( empty( $user_licenses ) ) {
19879 // If the context user doesn't have any license, don't update the licenses collection.
19880 return;
19881 }
19882
19883 $new_user_licenses_map = array();
19884 foreach ( $user_licenses as $user_license ) {
19885 $new_user_licenses_map[ $user_license->id ] = $user_license;
19886 }
19887
19888 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
19889
19890 // Update user licenses.
19891 $licenses_to_update_count = count( $new_user_licenses_map );
19892 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
19893 if ( 0 === $licenses_to_update_count ) {
19894 break;
19895 }
19896
19897 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
19898 // Update license.
19899 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
19900 unset( $new_user_licenses_map[ $license->id ] );
19901
19902 $licenses_to_update_count --;
19903 }
19904 }
19905
19906 if ( ! empty( $new_user_licenses_map ) ) {
19907 // Add new licenses.
19908 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
19909 }
19910
19911 $licenses = $all_licenses[ $module_id ];
19912 }
19913
19914 if ( ! isset( $all_licenses[ $module_id ] ) ) {
19915 $all_licenses[ $module_id ] = array();
19916 }
19917
19918 $all_licenses[ $module_id ] = $licenses;
19919
19920 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
19921 }
19922
19923 /**
19924 * Update user information.
19925 *
19926 * @author Vova Feldman (@svovaf)
19927 * @since 1.0.1
19928 *
19929 * @param bool $store Flush to Database if true.
19930 */
19931 private function _store_user( $store = true ) {
19932 $this->_logger->entrance();
19933
19934 if ( empty( $this->_user->id ) ) {
19935 $this->_logger->error( "Empty user ID, can't store user." );
19936
19937 return;
19938 }
19939
19940 $users = self::get_all_users();
19941 $users[ $this->_user->id ] = $this->_user;
19942 self::$_accounts->set_option( 'users', $users, $store );
19943 }
19944
19945 /**
19946 * Update new updates information.
19947 *
19948 * @author Vova Feldman (@svovaf)
19949 * @since 1.0.4
19950 *
19951 * @param FS_Plugin_Tag|null $update
19952 * @param bool $store Flush to Database if true.
19953 * @param bool|number $plugin_id
19954 */
19955 private function _store_update( $update, $store = true, $plugin_id = false ) {
19956 $this->_logger->entrance();
19957
19958 if ( $update instanceof FS_Plugin_Tag ) {
19959 $update->updated = time();
19960 }
19961
19962 if ( ! is_numeric( $plugin_id ) ) {
19963 $plugin_id = $this->_plugin->id;
19964 }
19965
19966 $updates = self::get_all_updates();
19967 $updates[ $plugin_id ] = $update;
19968 self::$_accounts->set_option( 'updates', $updates, $store );
19969 }
19970
19971 /**
19972 * Update new updates information.
19973 *
19974 * @author Vova Feldman (@svovaf)
19975 * @since 1.0.6
19976 *
19977 * @param FS_Plugin[] $plugin_addons
19978 * @param bool $store Flush to Database if true.
19979 */
19980 private function _store_addons( $plugin_addons, $store = true ) {
19981 $this->_logger->entrance();
19982
19983 $addons = self::get_all_addons();
19984 $addons[ $this->_plugin->id ] = $plugin_addons;
19985 self::$_accounts->set_option( 'addons', $addons, $store );
19986 }
19987
19988 /**
19989 * Delete plugin's associated add-ons.
19990 *
19991 * @author Vova Feldman (@svovaf)
19992 * @since 1.0.8
19993 *
19994 * @param bool $store
19995 *
19996 * @return bool
19997 */
19998 private function _delete_account_addons( $store = true ) {
19999 $all_addons = self::get_all_account_addons();
20000
20001 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20002 return false;
20003 }
20004
20005 unset( $all_addons[ $this->_plugin->id ] );
20006
20007 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20008
20009 return true;
20010 }
20011
20012 /**
20013 * Update account add-ons list.
20014 *
20015 * @author Vova Feldman (@svovaf)
20016 * @since 1.0.6
20017 *
20018 * @param FS_Plugin[] $addons
20019 * @param bool $store Flush to Database if true.
20020 */
20021 private function _store_account_addons( $addons, $store = true ) {
20022 $this->_logger->entrance();
20023
20024 $all_addons = self::get_all_account_addons();
20025 $all_addons[ $this->_plugin->id ] = $addons;
20026 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20027 }
20028
20029 /**
20030 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20031 * the valid user licenses will be fetched again and the account add-ons may be updated.
20032 *
20033 * @author Leo Fajardo (@leorw)
20034 * @since 2.2.4
20035 */
20036 private function purge_valid_user_licenses_cache() {
20037 if ( ! $this->is_registered() ) {
20038 return;
20039 }
20040
20041 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20042 }
20043
20044 /**
20045 * @author Leo Fajardo (@leorw)
20046 * @since 2.3.0
20047 *
20048 * @param array $all_licenses
20049 * @param number|null $site_license_id
20050 * @param bool $include_parent_licenses
20051 *
20052 * @return array
20053 */
20054 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20055 $foreign_licenses = array(
20056 'ids' => array(),
20057 'license_keys' => array()
20058 );
20059
20060 $parent_license_ids_map = array();
20061
20062 foreach ( $all_licenses as $license ) {
20063 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20064 continue;
20065 }
20066
20067 $foreign_licenses['ids'][] = $license->id;
20068 $foreign_licenses['license_keys'][] = $license->secret_key;
20069
20070 if (
20071 $include_parent_licenses &&
20072 is_object( $this->_license ) &&
20073 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20074 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20075 ) {
20076 /**
20077 * Include the parent license's info only if it has not been included before since child licenses
20078 * can have the same parent license.
20079 */
20080 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20081 $foreign_licenses['license_keys'][] = $license->secret_key;
20082
20083 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20084 }
20085 }
20086
20087 if ( empty( $foreign_licenses['ids'] ) ) {
20088 $foreign_licenses = array();
20089 }
20090
20091 return $foreign_licenses;
20092 }
20093
20094 /**
20095 * @author Leo Fajardo (@leorw)
20096 * @since 2.3.0
20097 *
20098 * @return string
20099 */
20100 private function get_valid_user_licenses_endpoint() {
20101 $user_licenses_endpoint = '/licenses.json?type=active' .
20102 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20103
20104 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20105
20106 if ( ! empty ( $foreign_licenses ) ) {
20107 $foreign_licenses = array(
20108 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20109 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20110 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20111 );
20112
20113 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20114 }
20115
20116 return $user_licenses_endpoint;
20117 }
20118
20119 /**
20120 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20121 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20122 * method filters out non–add-on product IDs and stores the add-on IDs.
20123 *
20124 * @author Leo Fajardo (@leorw)
20125 * @since 2.2.4
20126 *
20127 * @return stdClass[] array
20128 */
20129 private function fetch_valid_user_licenses() {
20130 $this->_logger->entrance();
20131
20132 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20133
20134 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20135 ! is_array( $result->licenses )
20136 ) {
20137 return array();
20138 }
20139
20140 return $result->licenses;
20141 }
20142
20143 /**
20144 * @author Leo Fajardo (@leorw)
20145 * @since 2.2.4
20146 *
20147 * @return number[] Account add-on IDs.
20148 */
20149 function get_updated_account_addons() {
20150 $addons = $this->get_addons();
20151 if ( empty( $addons ) ) {
20152 return array();
20153 }
20154
20155 $account_addons = $this->get_account_addons();
20156 if ( ! is_array( $account_addons ) ) {
20157 $account_addons = array();
20158 }
20159
20160 $user_licenses = $this->is_registered() ?
20161 $this->fetch_valid_user_licenses() :
20162 array();
20163
20164 if ( empty( $user_licenses ) ) {
20165 return $account_addons;
20166 }
20167
20168 $addon_ids = array();
20169 foreach ( $addons as $addon ) {
20170 $addon_ids[] = $addon->id;
20171 }
20172
20173 $license_product_ids = array();
20174
20175 foreach ( $user_licenses as $license ) {
20176 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20177 $license_product_ids = array_merge( $license_product_ids, $license->products );
20178 } else {
20179 $license_product_ids[] = $license->plugin_id;
20180 }
20181 }
20182
20183 // Filter out non–add-on IDs.
20184 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20185 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20186 $this->_store_account_addons( array_unique( $new_account_addons ) );
20187 }
20188
20189 return $new_account_addons;
20190 }
20191
20192 /**
20193 * Store account params in the Database.
20194 *
20195 * @author Vova Feldman (@svovaf)
20196 * @since 1.0.1
20197 *
20198 * @param null|int $blog_id Since 2.0.0
20199 */
20200 private function _store_account( $blog_id = null ) {
20201 $this->_logger->entrance();
20202
20203 $this->_store_site( false, $blog_id );
20204 $this->_store_user( false );
20205 $this->_store_plans( false );
20206 $this->_store_licenses( false );
20207
20208 self::$_accounts->store( $blog_id );
20209 }
20210
20211 /**
20212 * Sync user's information.
20213 *
20214 * @author Vova Feldman (@svovaf)
20215 * @since 1.0.3
20216 * @uses FS_Api
20217 */
20218 private function _handle_account_user_sync() {
20219 $this->_logger->entrance();
20220
20221 $api = $this->get_api_user_scope();
20222
20223 // Get user's information.
20224 $user = $api->get( '/', true );
20225
20226 if ( isset( $user->id ) ) {
20227 $this->_user->first = $user->first;
20228 $this->_user->last = $user->last;
20229 $this->_user->email = $user->email;
20230
20231 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20232
20233 if ( $user->is_verified &&
20234 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20235 ) {
20236 $this->_user->is_verified = true;
20237
20238 $this->do_action( 'account_email_verified', $user->email );
20239
20240 $this->_admin_notices->add(
20241 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20242 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20243 'success',
20244 // Make admin sticky if account menu item is invisible,
20245 // since the page will be auto redirected to the plugin's
20246 // main settings page, and the non-sticky message
20247 // will disappear.
20248 ! $is_menu_item_account_visible,
20249 'email_verified'
20250 );
20251 }
20252
20253 // Flush user details to DB.
20254 $this->_store_user();
20255
20256 $this->do_action( 'after_account_user_sync', $user );
20257
20258 /**
20259 * If account menu item is hidden, redirect to plugin's main settings page.
20260 *
20261 * @author Vova Feldman (@svovaf)
20262 * @since 1.1.6
20263 *
20264 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20265 */
20266 if ( ! $is_menu_item_account_visible ) {
20267 fs_redirect( $this->_get_admin_page_url() );
20268 }
20269 }
20270 }
20271
20272 /**
20273 * @author Vova Feldman (@svovaf)
20274 * @since 1.0.9
20275 * @uses FS_Api
20276 *
20277 * @param number|bool $license_id
20278 *
20279 * @return FS_Subscription|object|bool
20280 */
20281 private function _fetch_site_license_subscription( $license_id = false ) {
20282 $this->_logger->entrance();
20283 $api = $this->get_api_site_scope();
20284
20285 if ( ! is_numeric( $license_id ) ) {
20286 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20287 $this->_license->parent_license_id :
20288 $this->_license->id;
20289 }
20290
20291 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20292
20293 return ! isset( $result->error ) ?
20294 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20295 new FS_Subscription( $result->subscriptions[0] ) :
20296 false
20297 ) :
20298 $result;
20299 }
20300
20301 /**
20302 * @author Vova Feldman (@svovaf)
20303 * @since 1.0.4
20304 * @uses FS_Api
20305 *
20306 * @param number|bool $plan_id
20307 *
20308 * @return FS_Plugin_Plan|object
20309 */
20310 private function _fetch_site_plan( $plan_id = false ) {
20311 $this->_logger->entrance();
20312 $api = $this->get_api_site_scope();
20313
20314 if ( ! is_numeric( $plan_id ) ) {
20315 $plan_id = $this->_site->plan_id;
20316 }
20317
20318 $plan = $api->get( "/plans/{$plan_id}.json", true );
20319
20320 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20321 }
20322
20323 /**
20324 * @author Vova Feldman (@svovaf)
20325 * @since 1.0.5
20326 * @uses FS_Api
20327 *
20328 * @return FS_Plugin_Plan[]|object
20329 */
20330 private function _fetch_plugin_plans() {
20331 $this->_logger->entrance();
20332 $api = $this->get_current_or_network_user_api_scope();
20333
20334 /**
20335 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20336 */
20337 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20338
20339 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20340 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20341 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20342 }
20343
20344 $result = $result->plans;
20345 }
20346
20347 return $result;
20348 }
20349
20350 /**
20351 * @author Vova Feldman (@svovaf)
20352 * @since 2.0.0
20353 *
20354 * @param number $plan_id
20355 *
20356 * @return \FS_Plugin_Plan|object
20357 */
20358 private function fetch_plan_by_id( $plan_id ) {
20359 $this->_logger->entrance();
20360 $api = $this->get_current_or_network_user_api_scope();
20361
20362 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20363
20364 return $this->is_api_result_entity( $result ) ?
20365 new FS_Plugin_Plan( $result ) :
20366 $result;
20367 }
20368
20369 /**
20370 * @author Vova Feldman (@svovaf)
20371 * @since 1.0.5
20372 * @uses FS_Api
20373 *
20374 * @param number|bool $plugin_id
20375 * @param number|bool $site_license_id
20376 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20377 * @param number|null $blog_id
20378 *
20379 * @return FS_Plugin_License[]|object
20380 */
20381 private function _fetch_licenses(
20382 $plugin_id = false,
20383 $site_license_id = false,
20384 $foreign_licenses = array(),
20385 $blog_id = null
20386 ) {
20387 $this->_logger->entrance();
20388
20389 $api = $this->get_api_user_scope();
20390
20391 if ( ! is_numeric( $plugin_id ) ) {
20392 $plugin_id = $this->_plugin->id;
20393 }
20394
20395 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20396 if ( ! empty ( $foreign_licenses ) ) {
20397 $foreign_licenses = array(
20398 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20399 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20400 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20401 );
20402
20403 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20404 }
20405
20406 $result = $api->get( $user_licenses_endpoint, true );
20407
20408 $is_site_license_synced = false;
20409
20410 $api_errors = array();
20411
20412 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20413 is_array( $result->licenses )
20414 ) {
20415 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20416 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20417
20418 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20419 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20420 }
20421 }
20422
20423 $result = $result->licenses;
20424 } else {
20425 $api_errors[] = $result;
20426 $result = array();
20427 }
20428
20429 if ( ! $is_site_license_synced ) {
20430 if ( ! is_null( $blog_id ) ) {
20431 /**
20432 * If blog ID is not null, the request is for syncing of the license of a single site via the
20433 * network-level "Account" page.
20434 *
20435 * @author Leo Fajardo (@leorw)
20436 */
20437 $this->switch_to_blog( $blog_id );
20438 }
20439
20440 $api = $this->get_api_site_scope();
20441
20442 if ( is_numeric( $site_license_id ) ) {
20443 // Try to retrieve a foreign license that is linked to the install.
20444 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20445
20446 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20447 is_array( $api_result->licenses )
20448 ) {
20449 $licenses = $api_result->licenses;
20450
20451 if ( ! empty( $licenses ) ) {
20452 $result[] = new FS_Plugin_License( $licenses[0] );
20453 }
20454 } else {
20455 $api_errors[] = $api_result;
20456 }
20457 } else if (
20458 is_object( $this->_license ) &&
20459 /**
20460 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20461 * the FS instance that does the syncing is the parent FS instance.
20462 *
20463 * @author Leo Fajardo (@leorw)
20464 * @since 2.3.0
20465 */
20466 $this->_license->plugin_id == $plugin_id
20467 ) {
20468 $is_license_in_result = false;
20469 if ( ! empty( $result ) ) {
20470 foreach ( $result as $license ) {
20471 if ( $license->id == $this->_license->id ) {
20472 $is_license_in_result = true;
20473 break;
20474 }
20475 }
20476 }
20477
20478 if ( ! $is_license_in_result ) {
20479 // Fetch foreign license by ID and license key.
20480 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20481 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20482
20483 if ( $this->is_api_result_entity( $license ) ) {
20484 $result[] = new FS_Plugin_License( $license );
20485 } else {
20486 $api_errors[] = $license;
20487 }
20488 }
20489 }
20490
20491 if ( ! is_null( $blog_id ) ) {
20492 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20493 }
20494 }
20495
20496 if ( is_array( $result ) && 0 < count( $result ) ) {
20497 // If found at least one license, return license collection even if there are errors.
20498 return $result;
20499 }
20500
20501 if ( ! empty( $api_errors ) ) {
20502 // If found any errors and no licenses, return first error.
20503 return $api_errors[0];
20504 }
20505
20506 // Fallback to empty licenses list.
20507 return $result;
20508 }
20509
20510 /**
20511 * @author Vova Feldman (@svovaf)
20512 * @since 2.0.0
20513 *
20514 * @param number $license_id
20515 * @param string $license_key
20516 *
20517 * @return \FS_Plugin_License|object
20518 */
20519 private function fetch_license_by_key( $license_id, $license_key ) {
20520 $this->_logger->entrance();
20521
20522 $api = $this->get_current_or_network_user_api_scope();
20523
20524 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20525
20526 return $this->is_api_result_entity( $result ) ?
20527 new FS_Plugin_License( $result ) :
20528 $result;
20529 }
20530
20531 /**
20532 * @author Vova Feldman (@svovaf)
20533 * @since 1.2.0
20534 * @uses FS_Api
20535 *
20536 * @param number|bool $plugin_id
20537 * @param bool $flush
20538 *
20539 * @return FS_Payment[]|object
20540 */
20541 function _fetch_payments( $plugin_id = false, $flush = false ) {
20542 $this->_logger->entrance();
20543
20544 $api = $this->get_api_user_scope();
20545
20546 if ( ! is_numeric( $plugin_id ) ) {
20547 $plugin_id = $this->_plugin->id;
20548 }
20549
20550 $include_bundles = (
20551 is_object( $this->_plugin ) &&
20552 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20553 );
20554
20555 $result = $api->get(
20556 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20557 $flush
20558 );
20559
20560 if ( ! isset( $result->error ) ) {
20561 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20562 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20563 }
20564 $result = $result->payments;
20565 }
20566
20567 return $result;
20568 }
20569
20570 /**
20571 * @author Vova Feldman (@svovaf)
20572 * @since 1.2.1.5
20573 * @uses FS_Api
20574 *
20575 * @param bool $flush
20576 *
20577 * @return \FS_Billing|mixed
20578 */
20579 function _fetch_billing( $flush = false ) {
20580 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20581
20582 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20583
20584 if ( $this->is_api_result_entity( $billing ) ) {
20585 $billing = new FS_Billing( $billing );
20586 }
20587
20588 return $billing;
20589 }
20590
20591 /**
20592 * @author Vova Feldman (@svovaf)
20593 * @since 1.0.5
20594 *
20595 * @param FS_Plugin_License[] $licenses
20596 * @param number $module_id
20597 */
20598 private function _update_licenses( $licenses, $module_id ) {
20599 $this->_logger->entrance();
20600
20601 if ( is_array( $licenses ) ) {
20602 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20603 $licenses[ $i ]->updated = time();
20604 }
20605 }
20606
20607 $this->_store_licenses( true, $module_id, $licenses );
20608 }
20609
20610 /**
20611 * @author Vova Feldman (@svovaf)
20612 * @since 1.0.4
20613 *
20614 * @param bool|number $plugin_id
20615 * @param bool $flush Since 1.1.7.3
20616 * @param int $expiration Since 1.2.2.7
20617 * @param bool|string $newer_than Since 2.2.1
20618 * @param bool $fetch_upgrade_notice Since 2.12.1
20619 *
20620 * @return object|false New plugin tag info if exist.
20621 */
20622 private function _fetch_newer_version(
20623 $plugin_id = false,
20624 $flush = true,
20625 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
20626 $newer_than = false,
20627 $fetch_upgrade_notice = true
20628 ) {
20629 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than, false, $fetch_upgrade_notice );
20630
20631 if ( ! is_object( $latest_tag ) ) {
20632 return false;
20633 }
20634
20635 $plugin_version = $this->get_plugin_version();
20636
20637 // Check if version is actually newer.
20638 $has_new_version =
20639 // If it's an non-installed add-on then always return latest.
20640 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20641 // Compare versions.
20642 version_compare( $plugin_version, $latest_tag->version, '<' );
20643
20644 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20645
20646 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20647
20648 $this->_storage->beta_data = array(
20649 'is_beta' => $is_latest_version_beta,
20650 'version' => $latest_tag->version
20651 );
20652
20653 return $has_new_version ? $latest_tag : false;
20654 }
20655
20656 /**
20657 * @author Vova Feldman (@svovaf)
20658 * @since 1.0.5
20659 *
20660 * @param bool|number $plugin_id
20661 * @param bool $flush Since 1.1.7.3
20662 *
20663 * @return bool|FS_Plugin_Tag
20664 */
20665 function get_update( $plugin_id = false, $flush = true ) {
20666 $this->_logger->entrance();
20667
20668 if ( ! is_numeric( $plugin_id ) ) {
20669 $plugin_id = $this->_plugin->id;
20670 }
20671
20672 $this->check_updates( true, $plugin_id, $flush );
20673
20674 $updates = $this->get_all_updates();
20675
20676 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
20677 }
20678
20679 /**
20680 * Check if site assigned with active license.
20681 *
20682 * @author Vova Feldman (@svovaf)
20683 * @since 1.0.6
20684 *
20685 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
20686 */
20687 function has_active_license() {
20688 return (
20689 is_object( $this->_license ) &&
20690 is_numeric( $this->_license->id ) &&
20691 ! $this->_license->is_expired()
20692 );
20693 }
20694
20695 /**
20696 * Check if site assigned with active & valid (not expired) license.
20697 *
20698 * @author Vova Feldman (@svovaf)
20699 * @since 1.2.1
20700 *
20701 * @param bool $check_expiration
20702 */
20703 function has_active_valid_license( $check_expiration = true ) {
20704 return self::is_active_valid_license( $this->_license, $check_expiration );
20705 }
20706
20707 /**
20708 * @author Leo Fajardo (@leorw)
20709 * @since 2.3.1
20710 */
20711 function is_data_debug_mode() {
20712 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
20713 return false;
20714 }
20715
20716 $fs = $this->is_addon() ?
20717 $this->get_parent_instance() :
20718 $this;
20719
20720 if ( $fs->is_network_active() && fs_is_network_admin() ) {
20721 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
20722 } else {
20723 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
20724 }
20725
20726 return ( 'true' === $is_developer_license_debug_mode );
20727 }
20728
20729 /**
20730 * @author Leo Fajardo (@leorw)
20731 * @since 2.3.1
20732 */
20733 function _set_data_debug_mode() {
20734 if ( ! $this->is_whitelabeled( true ) ) {
20735 return;
20736 }
20737
20738 $license_or_user_key = fs_request_get_raw( 'license_or_user_key' );
20739
20740 $transient_value = ( ! empty( $license_or_user_key ) ) ?
20741 'true' :
20742 'false';
20743
20744 if ( 'true' === $transient_value ) {
20745 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
20746 'last_license_key' :
20747 'last_license_user_key'
20748 );
20749
20750 if ( md5( $license_or_user_key ) !== $stored_key ) {
20751 $this->shoot_ajax_failure( sprintf(
20752 '%s... %s',
20753 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
20754 $this->get_text_inline(
20755 'seems like the key you entered doesn\'t match our records.',
20756 'developer-or-license-not-found'
20757 )
20758 ) );
20759 }
20760 }
20761
20762 if ( $this->is_network_active() && fs_is_network_admin() ) {
20763 set_site_transient(
20764 "fs_{$this->get_id()}_data_debug_mode",
20765 $transient_value,
20766 WP_FS__TIME_24_HOURS_IN_SEC / 24
20767 );
20768 } else {
20769 set_transient(
20770 "fs_{$this->get_id()}_data_debug_mode",
20771 $transient_value,
20772 WP_FS__TIME_24_HOURS_IN_SEC / 24
20773 );
20774 }
20775
20776 if ( 'true' === $transient_value ) {
20777 $this->_admin_notices->add_sticky(
20778 $this->get_text_inline(
20779 '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.',
20780 'data_debug_mode_enabled'
20781 ),
20782 'data_debug_mode_enabled'
20783 );
20784 }
20785
20786 $this->shoot_ajax_success();
20787 }
20788
20789 /**
20790 * Check if a given license is active & valid (not expired).
20791 *
20792 * @author Vova Feldman (@svovaf)
20793 * @since 2.1.3
20794 *
20795 * @param FS_Plugin_License $license
20796 * @param bool $check_expiration
20797 *
20798 * @return bool
20799 */
20800 private static function is_active_valid_license( $license, $check_expiration = true ) {
20801 return (
20802 is_object( $license ) &&
20803 FS_Plugin_License::is_valid_id( $license->id ) &&
20804 $license->is_active() &&
20805 ( ! $check_expiration || $license->is_valid() )
20806 );
20807 }
20808
20809 /**
20810 * Checks if there's any site that is associated with an active & valid license.
20811 * This logic is used to determine if the admin can download the premium code base from a network level admin.
20812 *
20813 * @author Vova Feldman (@svovaf)
20814 * @since 2.1.3
20815 *
20816 * @return bool
20817 */
20818 function has_any_active_valid_license() {
20819 if ( ! fs_is_network_admin() ) {
20820 return $this->has_active_valid_license();
20821 }
20822
20823 $installs = $this->get_blog_install_map();
20824 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
20825
20826 foreach ( $installs as $blog_id => $install ) {
20827 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
20828 continue;
20829 }
20830
20831 foreach ( $all_plugin_licenses as $license ) {
20832 if ( $license->id == $install->license_id ) {
20833 if ( self::is_active_valid_license( $license ) ) {
20834 return true;
20835 }
20836 }
20837 }
20838 }
20839
20840 return false;
20841 }
20842
20843 /**
20844 * Check if site assigned with license with enabled features.
20845 *
20846 * @author Vova Feldman (@svovaf)
20847 * @since 1.0.6
20848 *
20849 * @return bool
20850 */
20851 function has_features_enabled_license() {
20852 return (
20853 is_object( $this->_license ) &&
20854 is_numeric( $this->_license->id ) &&
20855 $this->_license->is_features_enabled()
20856 );
20857 }
20858
20859 /**
20860 * Checks if the product is activated with a bundle license.
20861 *
20862 * @author Leo Fajardo (@leorw)
20863 * @since 2.4.0
20864 *
20865 * @return bool
20866 */
20867 function is_activated_with_bundle_license() {
20868 if ( ! $this->has_features_enabled_license() ) {
20869 return false;
20870 }
20871
20872 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
20873 }
20874
20875 /**
20876 * Check if user is a trial or have feature enabled license.
20877 *
20878 * @author Vova Feldman (@svovaf)
20879 * @since 1.1.7
20880 *
20881 * @return bool
20882 */
20883 function can_use_premium_code() {
20884 return $this->is_trial() || $this->has_features_enabled_license();
20885 }
20886
20887 /**
20888 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
20889 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
20890 * the context user is authenticated.
20891 *
20892 * @author Leo Fajardo (@leorw)
20893 * @since 1.2.2
20894 *
20895 * @return bool
20896 */
20897 function is_user_admin() {
20898 /**
20899 * Require a super-admin when network activated, running from the network level OR if
20900 * running from the site level but not delegated the opt-in.
20901 *
20902 * @author Vova Feldman (@svovaf)
20903 * @since 2.0.0
20904 */
20905 if ( $this->_is_network_active &&
20906 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
20907 ) {
20908 return is_super_admin();
20909 }
20910
20911 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
20912 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
20913 }
20914
20915 /**
20916 * Sync site's plan.
20917 *
20918 * @author Vova Feldman (@svovaf)
20919 * @since 1.0.3
20920 *
20921 * @uses FS_Api
20922 *
20923 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
20924 * the admin.
20925 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
20926 * network-level "Account" page.
20927 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
20928 * `_sync_plugin_license` method in order to switch to the previous blog when sending
20929 * updates for a single site in case `execute_cron` has switched to a different blog.
20930 */
20931 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
20932 $this->_logger->entrance();
20933
20934 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20935
20936 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
20937
20938 if ( $is_addon_sync ) {
20939 $this->_sync_addon_license( $plugin_id, $background );
20940 } else {
20941 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
20942 }
20943
20944 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
20945 }
20946
20947 /**
20948 * Sync plugin's add-on license.
20949 *
20950 * @author Vova Feldman (@svovaf)
20951 * @since 1.0.6
20952 * @uses FS_Api
20953 *
20954 * @param number $addon_id
20955 * @param bool $background
20956 */
20957 private function _sync_addon_license( $addon_id, $background ) {
20958 $this->_logger->entrance();
20959
20960 if ( $this->is_addon_activated( $addon_id ) ) {
20961 // If already installed, use add-on sync.
20962 $fs_addon = self::get_instance_by_id( $addon_id );
20963
20964 if (
20965 // Add-on is network activated and network integrated.
20966 $fs_addon->is_network_active() ||
20967 // Background sync cron.
20968 self::is_cron() ||
20969 // Add-on is not network activated or not network integrated.
20970 ! fs_is_network_admin()
20971 ) {
20972 $fs_addon->_sync_license( $background );
20973
20974 return;
20975 }
20976 }
20977
20978 // Validate add-on exists.
20979 $addon = $this->get_addon( $addon_id );
20980
20981 if ( ! is_object( $addon ) ) {
20982 return;
20983 }
20984
20985 // Add add-on into account add-ons.
20986 $account_addons = $this->get_account_addons();
20987 if ( ! is_array( $account_addons ) ) {
20988 $account_addons = array();
20989 }
20990 $account_addons[] = $addon->id;
20991 $account_addons = array_unique( $account_addons );
20992 $this->_store_account_addons( $account_addons );
20993
20994 // Load add-on licenses.
20995 $licenses = $this->_fetch_licenses( $addon->id );
20996
20997 // Sync add-on licenses.
20998 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
20999 $this->_update_licenses( $licenses, $addon->id );
21000
21001 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21002 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21003
21004 if ( ! isset( $plans_result->error ) ) {
21005 $plans = array();
21006 foreach ( $plans_result->plans as $plan ) {
21007 $plans[] = new FS_Plugin_Plan( $plan );
21008 }
21009
21010 $this->_admin_notices->add_sticky(
21011 sprintf(
21012 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21013 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21014 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21015 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21016 $addon->title
21017 ) . ' ' . $this->get_latest_download_link(
21018 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21019 $addon_id
21020 ),
21021 'addon_plan_upgraded_' . $addon->slug,
21022 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21023 );
21024 }
21025 }
21026 }
21027 }
21028
21029 /**
21030 * Sync site's plugin plan.
21031 *
21032 * @author Vova Feldman (@svovaf)
21033 * @since 1.0.6
21034 * @uses FS_Api
21035 *
21036 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21037 * @param bool $send_installs_update Since 2.0.0
21038 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21039 * syncing its license from the network-level "Account" page (e.g.: after
21040 * activating a license only for the single install).
21041 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21042 * can be used here to switch to the previous blog in case `execute_cron`
21043 * has switched to a different blog.
21044 */
21045 private function _sync_plugin_license(
21046 $background = false,
21047 $send_installs_update = true,
21048 $is_context_single_site = false,
21049 $current_blog_id = null
21050 ) {
21051 $this->_logger->entrance();
21052
21053 $plan_change = 'none';
21054
21055 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21056
21057 if ( ! $send_installs_update ) {
21058 $site = $this->_site;
21059 } else {
21060 /**
21061 * Sync site info.
21062 *
21063 * @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.
21064 */
21065 if ( $is_site_level_sync ) {
21066 /**
21067 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21068 *
21069 * @author Leo Fajardo (@leorw)
21070 * @since 2.2.3
21071 */
21072 if ( is_numeric( $current_blog_id ) ) {
21073 $this->switch_to_blog( $current_blog_id );
21074 }
21075
21076 $result = $this->send_install_update( array(), true, true );
21077 $is_valid = $this->is_api_result_entity( $result );
21078 } else {
21079 $result = $this->send_installs_update( array(), true, true );
21080 $is_valid = $this->is_api_result_object( $result, 'installs' );
21081 }
21082
21083 if ( ! $is_valid ) {
21084 if ( $is_context_single_site ) {
21085 // Switch back to the main blog so that the following logic will have the right entities.
21086 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21087 }
21088
21089 // Show API message only if not background sync or if paying customer.
21090 if ( ! $background || $this->is_paying() ) {
21091 // Try to ping API to see if not blocked.
21092 if ( FS_Api::is_blocked( $result ) ) {
21093 /**
21094 * @author Vova Feldman (@svovaf)
21095 * @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.
21096 */
21097 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21098 // Add notice immediately if not a background sync.
21099 $add_notice = ( ! $background );
21100
21101 if ( ! $add_notice ) {
21102 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21103
21104 // We only want to add the notice after 3 consecutive failures.
21105 $add_notice = ( 3 <= $counter );
21106
21107 if ( ! $add_notice ) {
21108 /**
21109 * Update counter transient only if notice shouldn't be added. If it is added the transient will be reset anyway, because the retries mechanism should only start counting if the admin isn't aware of the connectivity issue.
21110 *
21111 * Also, since the background sync happens once a day, setting the transient expiration for a week should be enough to count 3 failures, if there's an actual connectivity issue.
21112 */
21113 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21114 }
21115 }
21116
21117 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21118 if ( $add_notice ) {
21119 self::$_global_admin_notices->add(
21120 $this->generate_api_blocked_notice_message_from_result( $result ),
21121 '',
21122 'error',
21123 $background,
21124 'api_blocked'
21125 );
21126
21127 add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
21128
21129 // Notice was just shown, reset connectivity counter.
21130 delete_transient( '_fs_api_connection_retry_counter' );
21131 }
21132 }
21133 } else if ( is_object( $result ) ) {
21134 // Authentication params are broken.
21135 $this->_admin_notices->add(
21136 $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 ),
21137 '',
21138 'error'
21139 );
21140 }
21141 }
21142
21143 // No reason to continue with license sync while there are API issues.
21144 return;
21145 }
21146
21147 // API is working now. Delete the transient and start afresh.
21148 delete_transient('_fs_api_connection_retry_counter');
21149
21150 if ( $is_site_level_sync ) {
21151 $site = new FS_Site( $result );
21152 } else {
21153 // Map site addresses to their blog IDs.
21154 $address_to_blog_map = $this->get_address_to_blog_map();
21155
21156 // Find the current context install.
21157 $site = null;
21158 foreach ( $result->installs as $install ) {
21159 if ( $install->id == $this->_site->id ) {
21160 $site = new FS_Site( $install );
21161 } else {
21162 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21163 $blog_id = $address_to_blog_map[ $address ];
21164
21165 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21166 }
21167 }
21168 }
21169
21170 // Sync plans.
21171 $this->_sync_plans();
21172 }
21173
21174 // Remove sticky API connectivity message.
21175 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21176
21177 if ( ! $this->has_paid_plan() ) {
21178 $this->_site = $site;
21179 $this->_store_site(
21180 true,
21181 $is_site_level_sync ?
21182 null :
21183 $this->get_network_install_blog_id()
21184 );
21185 } else {
21186 $context_blog_id = 0;
21187
21188 if ( $is_context_single_site ) {
21189 $context_blog_id = get_current_blog_id();
21190
21191 // Switch back to the main blog in order to properly sync the license.
21192 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21193 }
21194
21195 /**
21196 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21197 * associated with that ID is not included in the user's licenses collection.
21198 * Save previous value to manage remote license renewals.
21199 */
21200 $was_license_expired_before_sync = is_object( $this->_license ) && $this->_license->is_expired();
21201 $this->_sync_licenses(
21202 $site->license_id,
21203 ( $is_context_single_site ?
21204 $context_blog_id :
21205 null
21206 )
21207 );
21208
21209 if ( $is_context_single_site ) {
21210 $this->switch_to_blog( $context_blog_id );
21211 }
21212
21213 // Check if plan / license changed.
21214 if ( $site->plan_id != $this->_site->plan_id ||
21215 // Check if trial started.
21216 $site->trial_plan_id != $this->_site->trial_plan_id ||
21217 $site->trial_ends != $this->_site->trial_ends ||
21218 // Check if license changed.
21219 $site->license_id != $this->_site->license_id
21220 ) {
21221 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21222 // New trial started.
21223 $this->_site = $site;
21224 $plan_change = 'trial_started';
21225
21226 // For trial with subscription use-case.
21227 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21228
21229 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21230 $this->_site = $site;
21231 $this->_update_site_license( $new_license );
21232 $this->_store_licenses();
21233
21234 $this->_sync_site_subscription( $this->_license );
21235 }
21236 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21237 // Was in trial, but now trial expired and no license ID.
21238 // New trial started.
21239 $this->_site = $site;
21240 $plan_change = 'trial_expired';
21241 } else {
21242 $is_free = $this->is_free_plan();
21243
21244 // Make sure license exist and not expired.
21245 $new_license = is_null( $site->license_id ) ?
21246 null :
21247 $this->_get_license_by_id( $site->license_id );
21248
21249 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21250 // License cancelled.
21251 $this->_site = $site;
21252 $this->_update_site_license( $new_license );
21253 $this->_store_licenses();
21254
21255 $plan_change = 'cancelled';
21256 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21257 // The license is expired, so ignore upgrade method.
21258 $this->_site = $site;
21259 } else {
21260 // License changed.
21261 $this->_site = $site;
21262
21263 /**
21264 * IMPORTANT:
21265 * 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.
21266 *
21267 * @author Vova Feldman (@svovaf)
21268 * @since 2.0.0
21269 */
21270 $this->_update_site_license( $new_license );
21271
21272 if ( ! $is_context_single_site &&
21273 fs_is_network_admin() &&
21274 $this->_is_network_active &&
21275 $new_license->quota > 1 &&
21276 get_blog_count() > 1
21277 ) {
21278 // See if license can activated on all sites.
21279 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21280 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21281 // Open the license activation dialog box on the account page.
21282 add_action( 'admin_footer', array(
21283 &$this,
21284 '_open_license_activation_dialog_box'
21285 ) );
21286 }
21287 }
21288 }
21289
21290 $this->_store_licenses();
21291
21292 $plan_change = $is_free ?
21293 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21294 ( is_object( $new_license ) ?
21295 'changed' :
21296 'downgraded' );
21297 }
21298 }
21299
21300 // Store updated site info.
21301 $this->_store_site(
21302 true,
21303 $is_site_level_sync ?
21304 null :
21305 $this->get_network_install_blog_id()
21306 );
21307 } else {
21308 if ( ! is_object( $this->_license ) ) {
21309 $this->maybe_update_whitelabel_flag(
21310 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21311 $this->get_license_by_id( $site->license_id ) :
21312 null
21313 );
21314 } else {
21315 $this->maybe_update_whitelabel_flag( $this->_license );
21316
21317 if ( $this->_license->is_expired() ) {
21318 if ( ! $this->has_features_enabled_license() ) {
21319 $this->_deactivate_license();
21320 $plan_change = 'downgraded';
21321 } else {
21322 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21323
21324 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21325 /**
21326 * Show the expired license notice every 14 days.
21327 *
21328 * @author Leo Fajardo (@leorw)
21329 * @since 2.3.1
21330 */
21331 $plan_change = 'expired';
21332 }
21333 }
21334 } else if ( $was_license_expired_before_sync ) {
21335 /**
21336 * If license was expired but it is not anymore.
21337 *
21338 *
21339 * @author Daniele Alessandra (@danielealessandra)
21340 */
21341 $plan_change = 'extended';
21342 }
21343 }
21344
21345 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21346 $this->_sync_site_subscription( $this->_license );
21347 }
21348 }
21349
21350 if ( ! $this->is_addon() &&
21351 $this->_site->is_beta() !== $site->is_beta()
21352 ) {
21353 // Beta flag updated.
21354 $this->_site = $site;
21355
21356 $this->_store_site(
21357 true,
21358 $is_site_level_sync ?
21359 null :
21360 $this->get_network_install_blog_id()
21361 );
21362 }
21363
21364 if ( $this->is_addon() || $this->has_addons() ) {
21365 /**
21366 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21367 * an updated valid user licenses collection will be fetched from the server which is used to also
21368 * update the account add-ons (add-ons the user has licenses for).
21369 *
21370 * @author Leo Fajardo (@leorw)
21371 * @since 2.2.4
21372 */
21373 $this->purge_valid_user_licenses_cache();
21374 }
21375 }
21376
21377 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21378
21379 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21380 switch ( $plan_change ) {
21381 case 'none':
21382 if ( ! $background && is_admin() ) {
21383 $plan = $this->is_trial() ?
21384 $this->get_trial_plan() :
21385 $this->get_plan();
21386
21387 if ( $plan->is_free() ) {
21388 $this->_admin_notices->add(
21389 sprintf(
21390 $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' ),
21391 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21392 ) . ' ' . sprintf(
21393 '<a href="%s">%s</a>',
21394 $this->contact_url(
21395 'bug',
21396 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' ),
21397 strtoupper( $plan->name )
21398 )
21399 ),
21400 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21401 ),
21402 $hmm_text
21403 );
21404 }
21405 }
21406 break;
21407 case 'upgraded':
21408 case 'activated':
21409 $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change );
21410
21411 $this->_admin_notices->remove_sticky( array(
21412 'trial_started',
21413 'trial_promotion',
21414 'trial_expired',
21415 'activation_complete',
21416 'license_expired',
21417 ) );
21418 break;
21419 case 'extended':
21420 $this->_admin_notices->remove_sticky( array(
21421 'trial_expired',
21422 'license_expired',
21423 ) );
21424 break;
21425 case 'changed':
21426 $this->_admin_notices->add_sticky(
21427 sprintf(
21428 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21429 $this->get_plan_title()
21430 ),
21431 'plan_changed'
21432 );
21433
21434 $this->_admin_notices->remove_sticky( array(
21435 'trial_started',
21436 'trial_promotion',
21437 'trial_expired',
21438 'activation_complete',
21439 ) );
21440 break;
21441 case 'downgraded':
21442 $this->_admin_notices->add_sticky(
21443 ($this->has_free_plan() ?
21444 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 ) :
21445 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21446 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) ) ),
21447 'license_expired',
21448 $hmm_text
21449 );
21450 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21451 break;
21452 case 'cancelled':
21453 $this->_admin_notices->add(
21454 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21455 sprintf(
21456 '<a href="%s">%s</a>',
21457 $this->contact_url( 'bug' ),
21458 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21459 ),
21460 $hmm_text,
21461 'error'
21462 );
21463 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21464 break;
21465 case 'expired':
21466 $this->_admin_notices->add_sticky(
21467 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 ),
21468 'license_expired',
21469 $hmm_text
21470 );
21471
21472 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21473
21474 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21475 break;
21476 case 'trial_started':
21477 $this->add_complete_upgrade_instructions_notice(
21478 sprintf(
21479 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21480 '<i>' . $this->get_plugin_name() . '</i>'
21481 ),
21482 'trial_started',
21483 $this->get_trial_plan()->title
21484 );
21485
21486 $this->_admin_notices->remove_sticky( array(
21487 'trial_promotion',
21488 ) );
21489 break;
21490 case 'trial_expired':
21491 $this->_admin_notices->add_sticky(
21492 ($this->has_free_plan() ?
21493 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21494 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21495 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))),
21496 'trial_expired',
21497 $hmm_text
21498 );
21499 $this->_admin_notices->remove_sticky( array(
21500 'trial_started',
21501 'trial_promotion',
21502 'plan_upgraded',
21503 ) );
21504 break;
21505 }
21506 }
21507
21508 if ( 'none' !== $plan_change ) {
21509 if (
21510 ! is_object( $this->_license ) ||
21511 ! $this->_license->is_whitelabeled
21512 ) {
21513 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21514 }
21515
21516 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21517 }
21518 }
21519
21520 /**
21521 * @author Leo Fajardo (@leorw)
21522 * @since 2.5.4
21523 *
21524 * @param mixed $result
21525 *
21526 * @return string
21527 */
21528 private function generate_api_blocked_notice_message_from_result( $result ) {
21529 $api_domains = $this->apply_filters( 'api_domains', array(
21530 'api.freemius.com',
21531 'wp.freemius.com',
21532 ) );
21533
21534 $api_domains_list_items = '';
21535
21536 foreach( $api_domains as $api_domain ) {
21537 $api_domains_list_items .= "<li>{$api_domain}</li>";
21538 }
21539
21540 $error_message = sprintf(
21541 $this->get_text_inline( 'Your server is blocking the access to Freemius\' API, which is crucial for %1$s synchronization. Please contact your host to whitelist the following domains:%2$s', 'server-blocking-access' ),
21542 $this->get_plugin_name(),
21543 "<ol>{$api_domains_list_items}</ol><a href='#' class='fs-api-request-error-show-details-link'>" . $this->get_text_inline( 'Show error details', 'show-error-details' ) . " <span class='dashicons dashicons-arrow-down-alt2'></span></a>"
21544 );
21545
21546 $error_message =
21547 "<div>{$error_message}</div>" .
21548 '<div class="fs-api-request-error-details" style="display: none">' .
21549 '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' .
21550 $result->error->message .
21551 '</div>';
21552
21553 return $error_message;
21554 }
21555
21556 /**
21557 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21558 *
21559 * @author Vova Feldman (@svovaf)
21560 * @since 2.0.0
21561 */
21562 public function _open_license_activation_dialog_box() {
21563 $vars = array( 'license_id' => $this->_site->license_id );
21564 fs_require_once_template( 'js/open-license-activation.php', $vars );
21565 }
21566
21567 /**
21568 * @author Vova Feldman (@svovaf)
21569 * @since 1.0.5
21570 *
21571 * @param bool $background
21572 * @param FS_Plugin_License|null $premium_license
21573 */
21574 protected function _activate_license( $background = false, $premium_license = null ) {
21575 $this->_logger->entrance();
21576
21577 if ( is_null( $premium_license ) ) {
21578 $license_id = fs_request_get( 'license_id' );
21579
21580 if ( is_object( $this->_site ) &&
21581 FS_Plugin_License::is_valid_id( $license_id ) &&
21582 $license_id == $this->_site->license_id
21583 ) {
21584 // License is already activated.
21585 return;
21586 }
21587
21588 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21589 $this->_get_license_by_id( $license_id ) :
21590 $this->_get_available_premium_license();
21591 }
21592
21593 if ( ! is_object( $premium_license ) ) {
21594 return;
21595 }
21596
21597 if ( ! is_object( $this->_site ) ) {
21598 // Not yet opted-in.
21599 $user = $this->get_current_or_network_user();
21600 if ( ! is_object( $user ) ) {
21601 $user = self::_get_user_by_id( $premium_license->user_id );
21602 }
21603
21604 if ( is_object( $user ) ) {
21605 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21606 } else {
21607 $this->opt_in(
21608 false,
21609 false,
21610 false,
21611 $premium_license->secret_key,
21612 false,
21613 false,
21614 false,
21615 null,
21616 array(),
21617 true,
21618 $premium_license->user_id
21619 );
21620
21621 return;
21622 }
21623 }
21624
21625
21626 /**
21627 * If the premium license is already associated with the install, just
21628 * update the license reference (activation is not required).
21629 *
21630 * @since 1.1.9
21631 */
21632 if ( $premium_license->id == $this->_site->license_id ) {
21633 // License is already activated.
21634 $this->_update_site_license( $premium_license );
21635 $this->_store_account();
21636
21637 return;
21638 }
21639
21640 if ( $this->_site->user_id != $premium_license->user_id ) {
21641 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21642 } else {
21643 $api_request_params = array();
21644 }
21645
21646 $api = $this->get_api_site_scope();
21647 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21648
21649 if ( ! $this->is_api_result_entity( $license ) ) {
21650 if ( ! $background ) {
21651 $this->_admin_notices->add( sprintf(
21652 '%s %s',
21653 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21654 ( is_object( $license ) && isset( $license->error ) ?
21655 $license->error->message :
21656 sprintf( '%s<br><code>%s</code>',
21657 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21658 var_export( $license, true )
21659 )
21660 )
21661 ),
21662 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21663 'error'
21664 );
21665 }
21666
21667 return;
21668 }
21669
21670 $premium_license = new FS_Plugin_License( $license );
21671
21672 // Updated site plan.
21673 $site = $this->get_api_site_scope()->get( '/', true );
21674 if ( $this->is_api_result_entity( $site ) ) {
21675 $this->_site = new FS_Site( $site );
21676 }
21677 $this->_update_site_license( $premium_license );
21678
21679 $this->_store_account();
21680
21681 if ( $this->is_addon() || $this->has_addons() ) {
21682 /**
21683 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21684 * an updated valid user licenses collection will be fetched from the server which is used to also
21685 * update the account add-ons (add-ons the user has licenses for).
21686 *
21687 * @author Leo Fajardo (@leorw)
21688 * @since 2.2.4
21689 */
21690 $this->purge_valid_user_licenses_cache();
21691 }
21692
21693 if ( ! $background ) {
21694 $this->add_complete_upgrade_instructions_notice(
21695 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ),
21696 'license_activated'
21697 );
21698 }
21699
21700 $this->_admin_notices->remove_sticky( array(
21701 'trial_promotion',
21702 'license_expired',
21703 ) );
21704 }
21705
21706 /**
21707 * @author Vova Feldman (@svovaf)
21708 * @since 1.0.5
21709 *
21710 * @param bool $show_notice
21711 */
21712 protected function _deactivate_license( $show_notice = true ) {
21713 $this->_logger->entrance();
21714
21715 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21716
21717 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
21718 $this->_admin_notices->add(
21719 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() ),
21720 $hmm_text
21721 );
21722
21723 return;
21724 }
21725
21726 $api = $this->get_api_site_scope();
21727 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
21728
21729 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
21730 }
21731
21732 /**
21733 * @author Leo Fajardo (@leorw)
21734 * @since 2.2.1
21735 *
21736 * @param FS_Plugin_License $license
21737 * @param bool|string $hmm_text
21738 * @param bool $show_notice
21739 */
21740 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
21741 if ( isset( $license->error ) ) {
21742 $this->_admin_notices->add(
21743 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
21744 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
21745 $hmm_text,
21746 'error'
21747 );
21748
21749 return;
21750 }
21751
21752 // Update license cache.
21753 if ( is_array( $this->_licenses ) ) {
21754 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
21755 if ( $license->id == $this->_licenses[ $i ]->id ) {
21756 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
21757 }
21758 }
21759 }
21760
21761 // Update site plan to default.
21762 $this->_sync_plans();
21763 $this->_site->plan_id = $this->_plans[0]->id;
21764 // Unlink license from site.
21765 $this->_update_site_license( null );
21766
21767 $this->_store_account();
21768
21769 if ( $show_notice ) {
21770 $this->_admin_notices->add(
21771 sprintf( $this->is_only_premium() ?
21772 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
21773 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
21774 $this->get_plan_title()
21775 ),
21776 $this->get_text_inline( 'O.K', 'ok' )
21777 );
21778 }
21779
21780 $this->_admin_notices->remove_sticky( array(
21781 'plan_upgraded',
21782 'license_activated',
21783 ) );
21784 }
21785
21786 /**
21787 * Site plan downgrade.
21788 *
21789 * @author Vova Feldman (@svovaf)
21790 * @since 1.0.4
21791 *
21792 * @return object
21793 *
21794 * @uses FS_Api
21795 */
21796 private function _downgrade_site() {
21797 $this->_logger->entrance();
21798
21799 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
21800
21801 $api = $this->get_api_site_scope();
21802 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
21803
21804 $plan_downgraded = false;
21805 $plan = false;
21806 if ( $this->is_api_result_entity( $site ) ) {
21807 $prev_plan_id = $this->_site->plan_id;
21808
21809 // Update new site plan id.
21810 $this->_site->plan_id = $site->plan_id;
21811
21812 $plan = $this->get_plan();
21813 $subscription = $this->_sync_site_subscription( $this->_license );
21814
21815 // Plan downgraded if plan was changed or subscription was cancelled.
21816 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
21817 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
21818 } else {
21819 // handle different error cases.
21820 $this->handle_license_deactivation_result(
21821 $site,
21822 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
21823 );
21824 }
21825
21826 if ( ! $plan_downgraded ) {
21827 return (object) array(
21828 'error' => (object) array(
21829 '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' )
21830 )
21831 );
21832 }
21833
21834 // Remove previous sticky message about upgrade (if exist).
21835 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21836
21837 $this->_admin_notices->add(
21838 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
21839 $plan->title,
21840 human_time_diff( time(), strtotime( $this->_license->expiration ) )
21841 )
21842 );
21843
21844 // Store site updates.
21845 $this->_store_site();
21846
21847 if ( $deactivate_license &&
21848 ! FS_Plugin_License::is_valid_id( $site->license_id )
21849 ) {
21850 if ( $this->_site->is_localhost() ) {
21851 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
21852 } else {
21853 $this->_license->activated = max( 0, $this->_license->activated - 1 );
21854 }
21855
21856 // Handle successful license deactivation result.
21857 $this->handle_license_deactivation_result( $this->_license );
21858 }
21859
21860 return $site;
21861 }
21862
21863 /**
21864 * @author Vova Feldman (@svovaf)
21865 * @since 1.1.8.1
21866 *
21867 * @param bool|string $plan_name
21868 * @param bool $add_sticky_notice
21869 *
21870 * @return bool If trial was successfully started.
21871 */
21872 function start_trial( $plan_name = false, $add_sticky_notice = false ) {
21873 $this->_logger->entrance();
21874
21875 // Alias.
21876 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
21877
21878 if ( $this->is_trial() ) {
21879 // Already in trial mode.
21880 $this->_admin_notices->add(
21881 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
21882 $oops_text,
21883 'error',
21884 $add_sticky_notice
21885 );
21886
21887 return false;
21888 }
21889
21890 if ( $this->_site->is_trial_utilized() && ! $this->is_payments_sandbox() ) {
21891 // Trial was already utilized.
21892 $this->_admin_notices->add(
21893 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
21894 $oops_text,
21895 'error',
21896 $add_sticky_notice
21897 );
21898
21899 return false;
21900 }
21901
21902 if ( false !== $plan_name ) {
21903 $plan = $this->get_plan_by_name( $plan_name );
21904
21905 if ( false === $plan ) {
21906 // Plan doesn't exist.
21907 $this->_admin_notices->add(
21908 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
21909 $oops_text,
21910 'error',
21911 $add_sticky_notice
21912 );
21913
21914 return false;
21915 }
21916
21917 if ( ! $plan->has_trial() ) {
21918 // Plan doesn't exist.
21919 $this->_admin_notices->add(
21920 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
21921 $oops_text,
21922 'error',
21923 $add_sticky_notice
21924 );
21925
21926 return false;
21927 }
21928 } else {
21929 if ( ! $this->has_trial_plan() ) {
21930 // None of the plans have a trial.
21931 $this->_admin_notices->add(
21932 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
21933 $oops_text,
21934 'error',
21935 $add_sticky_notice
21936 );
21937
21938 return false;
21939 }
21940
21941 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21942
21943 $plan = $plans_with_trial[0];
21944 }
21945
21946 $trial_params = array();
21947
21948 if ( $this->is_payments_sandbox() ) {
21949 $trial_params['trial_timestamp'] = time();
21950 $trial_params['trial_token'] = FS_Security::instance()->get_trial_token(
21951 $this->get_plugin(),
21952 $plan,
21953 $trial_params['trial_timestamp']
21954 );
21955 }
21956
21957 $api = $this->get_api_site_scope();
21958 $trial = $api->call( "plans/{$plan->id}/trials.json", 'post', $trial_params );
21959
21960 if ( ! $this->is_api_result_entity( $trial ) ) {
21961 // Some API error while trying to start the trial.
21962 $this->_admin_notices->add(
21963 $this->get_api_error_message( $trial ),
21964 $oops_text,
21965 'error',
21966 $add_sticky_notice
21967 );
21968
21969 return false;
21970 }
21971
21972 // Sync license.
21973 $this->_sync_license();
21974
21975 return $this->is_trial();
21976 }
21977
21978 /**
21979 * Cancel site trial.
21980 *
21981 * @author Vova Feldman (@svovaf)
21982 * @since 1.0.9
21983 *
21984 * @return object
21985 *
21986 * @uses FS_Api
21987 */
21988 private function _cancel_trial() {
21989 $this->_logger->entrance();
21990
21991 if ( ! $this->is_trial() ) {
21992 return (object) array(
21993 'error' => (object) array(
21994 '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' )
21995 )
21996 );
21997 }
21998
21999 $trial_plan = $this->get_trial_plan();
22000
22001 $api = $this->get_api_site_scope();
22002 $site = $api->call( 'trials.json', 'delete' );
22003
22004 $trial_cancelled = false;
22005
22006 if ( $this->is_api_result_entity( $site ) ) {
22007 $prev_trial_ends = $this->_site->trial_ends;
22008
22009 if ( $this->is_paid_trial() ) {
22010 $this->_license->expiration = $site->trial_ends;
22011 $this->_license->is_cancelled = true;
22012 $this->_update_site_license( $this->_license );
22013 $this->_store_licenses();
22014
22015 // Clear subscription reference.
22016 $this->_sync_site_subscription( null );
22017 }
22018
22019 // Update site info.
22020 $this->_site = new FS_Site( $site );
22021
22022 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22023 } else {
22024 // @todo handle different error cases.
22025 }
22026
22027 if ( ! $trial_cancelled ) {
22028 return (object) array(
22029 'error' => (object) array(
22030 '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' )
22031 )
22032 );
22033 }
22034
22035 // Remove previous sticky messages about upgrade or trial (if exist).
22036 $this->_admin_notices->remove_sticky( array(
22037 'trial_started',
22038 'trial_promotion',
22039 'plan_upgraded',
22040 ) );
22041
22042 // Store site updates.
22043 $this->_store_site();
22044
22045 if ( ! $this->is_addon() ||
22046 ! $this->deactivate_premium_only_addon_without_license( true )
22047 ) {
22048 $this->_admin_notices->add(
22049 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22050 );
22051 }
22052
22053 return $site;
22054 }
22055
22056 /**
22057 * @author Vova Feldman (@svovaf)
22058 * @since 1.0.6
22059 *
22060 * @param bool|number $plugin_id
22061 *
22062 * @return bool
22063 */
22064 private function _is_addon_id( $plugin_id ) {
22065 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22066 }
22067
22068 /**
22069 * Check if user eligible to download premium version updates.
22070 *
22071 * @author Vova Feldman (@svovaf)
22072 * @since 1.0.6
22073 *
22074 * @return bool
22075 */
22076 private function _can_download_premium() {
22077 return $this->has_any_active_valid_license() ||
22078 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22079 }
22080
22081 /**
22082 *
22083 * @author Vova Feldman (@svovaf)
22084 * @since 1.0.6
22085 *
22086 * @param bool|number $addon_id
22087 * @param string $type "json" or "zip"
22088 *
22089 * @return string
22090 */
22091 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22092
22093 $is_addon = $this->_is_addon_id( $addon_id );
22094
22095 $is_premium = null;
22096 if ( ! $is_addon ) {
22097 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22098 } else if ( $this->is_addon_activated( $addon_id ) ) {
22099 $fs_addon = self::get_instance_by_id( $addon_id );
22100 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22101 }
22102
22103 // If add-on, then append add-on ID.
22104 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22105 '/updates/latest.' . $type;
22106
22107 // If add-on and not yet activated, try to fetch based on server licensing.
22108 if ( is_bool( $is_premium ) ) {
22109 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22110 }
22111
22112 if ( $this->has_secret_key() ) {
22113 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22114 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22115 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22116 }
22117
22118 return $endpoint;
22119 }
22120
22121 /**
22122 * @author Vova Feldman (@svovaf)
22123 * @since 1.0.4
22124 *
22125 * @param bool|number $addon_id
22126 * @param bool $flush Since 1.1.7.3
22127 * @param int $expiration Since 1.2.2.7
22128 * @param bool|string $newer_than Since 2.2.1
22129 * @param bool|string $fetch_readme Since 2.2.1
22130 * @param bool $fetch_upgrade_notice Since 2.12.1
22131 *
22132 * @return object|false Plugin latest tag info.
22133 */
22134 function _fetch_latest_version(
22135 $addon_id = false,
22136 $flush = true,
22137 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22138 $newer_than = false,
22139 $fetch_readme = true,
22140 $fetch_upgrade_notice = false
22141 ) {
22142 $this->_logger->entrance();
22143
22144 if ( $this->is_unresolved_clone( true ) ) {
22145 return false;
22146 }
22147
22148 $switch_to_blog_id = null;
22149
22150 /**
22151 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22152 * @since 1.1.7.4 Also check updates for add-ons.
22153 */
22154 if (
22155 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22156 ! $this->_is_addon_id( $addon_id )
22157 ) {
22158 if ( ! is_multisite() ) {
22159 return false;
22160 }
22161
22162 $installs_map = $this->get_blog_install_map();
22163
22164 foreach ( $installs_map as $blog_id => $install ) {
22165 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22166 continue;
22167 }
22168
22169 /**
22170 * @var FS_Site $install
22171 */
22172 if ( $install->is_trial() ) {
22173 $switch_to_blog_id = $blog_id;
22174 break;
22175 }
22176
22177 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22178 $license = $this->get_license_by_id( $install->license_id );
22179
22180 if ( is_object( $license ) && $license->is_features_enabled() ) {
22181 $switch_to_blog_id = $blog_id;
22182 break;
22183 }
22184 }
22185 }
22186
22187 if ( is_null( $switch_to_blog_id ) ) {
22188 return false;
22189 }
22190 }
22191
22192 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22193 get_current_blog_id() :
22194 0;
22195
22196 if ( is_numeric( $switch_to_blog_id ) ) {
22197 $this->switch_to_blog( $switch_to_blog_id );
22198 }
22199
22200 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22201
22202 if ( ! empty( $newer_than ) ) {
22203 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22204 }
22205
22206 if ( true === $fetch_readme ) {
22207 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22208
22209 // Don't cache the API response when fetching readme information.
22210 $expiration = null;
22211 }
22212
22213 if ( true === $fetch_upgrade_notice ) {
22214 $latest_version_endpoint = add_query_arg( 'include_upgrade_notice', 'true', $latest_version_endpoint );
22215 }
22216
22217 $tag = $this->get_api_site_or_plugin_scope()->get(
22218 $latest_version_endpoint,
22219 $flush,
22220 $expiration
22221 );
22222
22223 if ( is_numeric( $switch_to_blog_id ) ) {
22224 $this->switch_to_blog( $current_blog_id );
22225 }
22226
22227 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22228
22229 $this->_logger->departure( 'Latest version ' . $latest_version );
22230
22231 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22232 }
22233
22234 #----------------------------------------------------------------------------------
22235 #region Download Plugin
22236 #----------------------------------------------------------------------------------
22237
22238 /**
22239 * Download latest plugin version, based on plan.
22240 *
22241 * Not like _download_latest(), this will redirect the page
22242 * to secure download url to prevent dual download (from FS to WP server,
22243 * and then from WP server to the client / browser).
22244 *
22245 * @author Vova Feldman (@svovaf)
22246 * @since 1.0.9
22247 *
22248 * @param bool|number $plugin_id
22249 *
22250 * @uses FS_Api
22251 * @uses wp_redirect()
22252 */
22253 private function download_latest_directly( $plugin_id = false ) {
22254 $this->_logger->entrance();
22255
22256 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22257 }
22258
22259 /**
22260 * Get latest plugin FS API download URL.
22261 *
22262 * @author Vova Feldman (@svovaf)
22263 * @since 1.0.9
22264 *
22265 * @param bool|number $plugin_id
22266 *
22267 * @return string
22268 */
22269 private function get_latest_download_api_url( $plugin_id = false ) {
22270 $this->_logger->entrance();
22271
22272 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22273 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22274 );
22275
22276 return str_replace( 'http:', 'https:', $download_api_url );
22277 }
22278
22279 /**
22280 * Get payment invoice URL.
22281 *
22282 * @author Vova Feldman (@svovaf)
22283 * @since 1.2.0
22284 *
22285 * @param bool|number $payment_id
22286 *
22287 * @return string
22288 */
22289 function _get_invoice_api_url( $payment_id = false ) {
22290 $this->_logger->entrance();
22291
22292 $url = $this->get_api_user_scope()->get_signed_url(
22293 "/payments/{$payment_id}/invoice.pdf"
22294 );
22295
22296 if ( ! fs_starts_with( $url, 'https://' ) ) {
22297 // Always use HTTPS for invoices.
22298 $url = 'https' . substr( $url, 4 );
22299 }
22300
22301 return $url;
22302 }
22303
22304 /**
22305 * Get latest plugin download link.
22306 *
22307 * @author Vova Feldman (@svovaf)
22308 * @since 1.0.9
22309 *
22310 * @param string $label
22311 * @param bool|number $plugin_id
22312 *
22313 * @return string
22314 */
22315 private function get_latest_download_link( $label, $plugin_id = false ) {
22316 return sprintf(
22317 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22318 $this->_get_latest_download_local_url( $plugin_id ),
22319 $label
22320 );
22321 }
22322
22323 /**
22324 * Get latest plugin download local URL.
22325 *
22326 * @author Vova Feldman (@svovaf)
22327 * @since 1.0.9
22328 *
22329 * @param bool|number $plugin_id
22330 *
22331 * @return string
22332 */
22333 function _get_latest_download_local_url( $plugin_id = false ) {
22334 // Add timestamp to protect from caching.
22335 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22336
22337 if ( ! empty( $plugin_id ) ) {
22338 $params['plugin_id'] = $plugin_id;
22339 } else if ( $this->is_addon() ) {
22340 $params['plugin_id'] = $this->get_id();
22341 }
22342
22343 $fs = $this->is_addon() ?
22344 $this->get_parent_instance() :
22345 $this;
22346
22347 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22348 }
22349
22350 #endregion Download Plugin ------------------------------------------------------------------
22351
22352 /**
22353 * @author Vova Feldman (@svovaf)
22354 * @since 1.0.4
22355 *
22356 * @uses FS_Api
22357 *
22358 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22359 * was initiated by the admin.
22360 * @param bool|number $plugin_id
22361 * @param bool $flush Since 1.1.7.3
22362 */
22363 private function check_updates( $background = false, $plugin_id = false, $flush = true ) {
22364 $this->_logger->entrance();
22365
22366 $newer_than = ( $this->is_premium() ? $this->get_plugin_version() : false );
22367
22368 // Check if there's a newer version for download.
22369 $new_version = $this->_fetch_newer_version(
22370 $plugin_id,
22371 $flush,
22372 FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
22373 $newer_than,
22374 ( false !== $newer_than )
22375 );
22376
22377 $update = null;
22378 if ( is_object( $new_version ) ) {
22379 $update = new FS_Plugin_Tag( $new_version );
22380
22381 if ( ! $background ) {
22382 $this->_admin_notices->add(
22383 sprintf(
22384 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22385 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22386 $update->version,
22387 sprintf(
22388 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22389 $this->get_account_url( 'download_latest' ),
22390 sprintf(
22391 /* translators: %s: plan name (e.g. latest "Professional" version) */
22392 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22393 $this->get_plan_title()
22394 )
22395 )
22396 ),
22397 $this->get_text_inline( 'New', 'new' ) . '!'
22398 );
22399 }
22400 } else if ( false === $new_version && ! $background ) {
22401 $this->_admin_notices->add(
22402 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22403 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22404 );
22405 }
22406
22407 $this->_store_update( $update, true, $plugin_id );
22408 }
22409
22410 /**
22411 * @author Vova Feldman (@svovaf)
22412 * @since 1.0.4
22413 *
22414 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22415 *
22416 * @return FS_Plugin[]
22417 *
22418 * @uses FS_Api
22419 */
22420 private function sync_addons( $flush = false ) {
22421 $this->_logger->entrance();
22422
22423 $api = $this->get_api_site_or_plugin_scope();
22424
22425 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22426
22427 /**
22428 * @since 1.2.1
22429 *
22430 * If there's a cached version of the add-ons and not asking
22431 * for a flush, just use the currently stored add-ons.
22432 */
22433 if ( ! $flush && $api->is_cached( $path ) ) {
22434 $addons = self::get_all_addons();
22435
22436 return isset( $addons[ $this->_plugin->id ] ) ?
22437 $addons[ $this->_plugin->id ] :
22438 array();
22439 }
22440
22441 $result = $api->get( $path, $flush );
22442
22443 $addons = array();
22444 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22445 is_array( $result->plugins )
22446 ) {
22447 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22448 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22449 }
22450
22451 $this->_store_addons( $addons, true );
22452 }
22453
22454 return $addons;
22455 }
22456
22457 /**
22458 * Handle user email update.
22459 *
22460 * @author Vova Feldman (@svovaf)
22461 * @since 1.0.3
22462 * @uses FS_Api
22463 *
22464 * @param string $new_email
22465 *
22466 * @return object
22467 */
22468 private function update_email( $new_email ) {
22469 $this->_logger->entrance();
22470
22471 $api = $this->get_api_user_scope();
22472 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22473 'email' => $new_email,
22474 'after_email_confirm_url' => $this->_get_admin_page_url(
22475 'account',
22476 array( 'fs_action' => 'sync_user' )
22477 ),
22478 ) );
22479
22480 if ( ! isset( $user->error ) ) {
22481 $this->_user->email = $user->email;
22482 $this->_user->is_verified = $user->is_verified;
22483 $this->_store_user();
22484 } else {
22485 // handle different error cases.
22486 }
22487
22488 return $user;
22489 }
22490
22491 #----------------------------------------------------------------------------------
22492 #region API Error Handling
22493 #----------------------------------------------------------------------------------
22494
22495 /**
22496 * @author Vova Feldman (@svovaf)
22497 * @since 1.1.1
22498 *
22499 * @param mixed $result
22500 *
22501 * @return bool Is API result contains an error.
22502 */
22503 private function is_api_error( $result ) {
22504 return FS_Api::is_api_error( $result );
22505 }
22506
22507 /**
22508 * Checks if given API result is a non-empty and not an error object.
22509 *
22510 * @author Vova Feldman (@svovaf)
22511 * @since 1.2.1.5
22512 *
22513 * @param mixed $result
22514 * @param string|null $required_property Optional property we want to verify that is set.
22515 *
22516 * @return bool
22517 */
22518 function is_api_result_object( $result, $required_property = null ) {
22519 return FS_Api::is_api_result_object( $result, $required_property );
22520 }
22521
22522 /**
22523 * Checks if given API result is a non-empty entity object with non-empty ID.
22524 *
22525 * @author Vova Feldman (@svovaf)
22526 * @since 1.2.1.5
22527 *
22528 * @param mixed $result
22529 *
22530 * @return bool
22531 */
22532 private function is_api_result_entity( $result ) {
22533 return FS_Api::is_api_result_entity( $result );
22534 }
22535
22536 #endregion
22537
22538 /**
22539 * Make sure a given argument is an array of a specific type.
22540 *
22541 * @author Vova Feldman (@svovaf)
22542 * @since 1.2.1.5
22543 *
22544 * @param mixed $array
22545 * @param string $class
22546 *
22547 * @return bool
22548 */
22549 private function is_array_instanceof( $array, $class ) {
22550 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22551 }
22552
22553 /**
22554 * Start install ownership change.
22555 *
22556 * @author Vova Feldman (@svovaf)
22557 * @since 1.1.1
22558 * @uses FS_Api
22559 *
22560 * @param string $candidate_email
22561 * @param string $transfer_type
22562 *
22563 * @return bool Is ownership change successfully initiated.
22564 */
22565 private function init_change_owner( $candidate_email, $transfer_type ) {
22566 $this->_logger->entrance();
22567
22568 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22569 $install_ids = array();
22570
22571 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22572 $install = $install_info['install'];
22573
22574 if ( $this->_user->id != $install->user_id ) {
22575 // Skip add-on installs that are not owned by the parent product's install's owner.
22576 continue;
22577 }
22578
22579 $install_ids[ $slug ] = $install->id;
22580 }
22581
22582 $api = $this->get_api_site_scope();
22583 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22584 'email' => $candidate_email,
22585 'transfer_type' => $transfer_type,
22586 'install_ids' => implode( ',', array_values( $install_ids ) ),
22587 'after_confirm_url' => $this->_get_admin_page_url(
22588 'account',
22589 array( 'fs_action' => 'change_owner' )
22590 ),
22591 ) );
22592
22593 return ! $this->is_api_error( $result );
22594 }
22595
22596 /**
22597 * Handle install ownership change.
22598 *
22599 * @author Vova Feldman (@svovaf)
22600 * @since 1.1.1
22601 * @uses FS_Api
22602 *
22603 * @return bool Was ownership change successfully complete.
22604 */
22605 private function complete_change_owner() {
22606 $this->_logger->entrance();
22607
22608 $install_ids = fs_request_get( 'install_ids' );
22609
22610 if ( ! empty( $install_ids ) ) {
22611 $install_ids = explode( ',', $install_ids );
22612
22613 foreach ( $install_ids as $key => $install_id ) {
22614 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22615 unset( $install_ids[ $key ] );
22616 }
22617 }
22618 }
22619
22620 if ( ! is_array( $install_ids ) ) {
22621 $install_ids = array();
22622 }
22623
22624 $user = new FS_User();
22625 $user->id = fs_request_get( 'user_id' );
22626 $user->public_key = fs_request_get_raw( 'user_public_key' );
22627 $user->secret_key = fs_request_get_raw( 'user_secret_key' );
22628
22629 $prev_user = $this->_user;
22630 $this->_user = $user;
22631
22632 $result = $this->get_api_user_scope( true )->get(
22633 "/installs.json?install_ids=" . implode( ',', $install_ids )
22634 );
22635
22636 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22637
22638 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22639 $site_id_slug_map = array();
22640
22641 foreach ( $current_blog_sites as $slug => $site ) {
22642 $site_id_slug_map[ $site->id ] = $slug;
22643 }
22644
22645 foreach ( $result->installs as $install ) {
22646 $site = new FS_Site( $install );
22647
22648 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22649 continue;
22650 }
22651
22652 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22653
22654 if ( $this->_site->id == $site->id ) {
22655 $this->_site = $site;
22656 }
22657 }
22658 }
22659
22660 // Validate install's user and given user.
22661 if ( $user->id != $this->_site->user_id ) {
22662 $this->_user = $prev_user;
22663
22664 return false;
22665 }
22666
22667 $this->set_account_option( 'sites', $current_blog_sites, true );
22668
22669 // Fetch new user information.
22670 $user_result = $this->get_api_user_scope( true )->get();
22671 $user = new FS_User( $user_result );
22672 $this->_user = $user;
22673
22674 $this->_set_account( $user, $this->_site );
22675
22676 $remove_user = true;
22677 $all_modules_sites = FS_DebugManager::get_all_modules_sites();
22678
22679 foreach ( $all_modules_sites as $sites_by_module_type ) {
22680 foreach ( $sites_by_module_type as $sites_by_slug ) {
22681 foreach ( $sites_by_slug as $site ) {
22682 if ( $prev_user->id == $site->user_id ) {
22683 $remove_user = false;
22684 break;
22685 }
22686 }
22687
22688 if ( ! $remove_user ) {
22689 break;
22690 }
22691 }
22692
22693 if ( ! $remove_user ) {
22694 break;
22695 }
22696 }
22697
22698 if ( $remove_user ) {
22699 $users = self::get_all_users();
22700
22701 if ( isset( $users[ $prev_user->id ] ) ) {
22702 unset( $users[ $prev_user->id ] );
22703 } else {
22704 // If the prev user wasn't found by the key, iterate over the users collection.
22705 foreach ( $users as $key => $user ) {
22706 if ( $user->id == $prev_user->id ) {
22707 unset( $users[ $key ] );
22708 break;
22709 }
22710 }
22711 }
22712
22713 $this->set_account_option( 'users', $users, true );
22714 }
22715
22716 return true;
22717 }
22718
22719 /**
22720 * Completes ownership change by license.
22721 *
22722 * @author Leo Fajardo (@leorw)
22723 * @since 2.3.2
22724 *
22725 * @param number $user_id
22726 * @param array[string]number $install_ids_by_slug_map
22727 *
22728 */
22729 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22730 $this->_logger->entrance();
22731
22732 $this->sync_user_by_current_install( $user_id );
22733
22734 $result = $this->get_api_user_scope( true )->get(
22735 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22736 );
22737
22738 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22739 $sites = self::get_all_sites( $this->get_module_type() );
22740 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22741
22742 foreach ( $result->installs as $install ) {
22743 $site = new FS_Site( $install );
22744
22745 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
22746 }
22747
22748 $this->set_account_option( 'sites', $sites, true );
22749 }
22750 }
22751
22752 /**
22753 * Handle user name update.
22754 *
22755 * @author Vova Feldman (@svovaf)
22756 * @since 1.0.9
22757 * @uses FS_Api
22758 *
22759 * @return object
22760 */
22761 private function update_user_name() {
22762 $this->_logger->entrance();
22763 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
22764
22765 $api = $this->get_api_user_scope();
22766 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
22767 'name' => $name,
22768 ) );
22769
22770 if ( ! isset( $user->error ) ) {
22771 $this->_user->first = $user->first;
22772 $this->_user->last = $user->last;
22773 $this->_store_user();
22774 } else {
22775 // handle different error cases.
22776
22777 }
22778
22779 return $user;
22780 }
22781
22782 /**
22783 * Verify user email.
22784 *
22785 * @author Vova Feldman (@svovaf)
22786 * @since 1.0.3
22787 * @uses FS_Api
22788 */
22789 private function verify_email() {
22790 $this->_handle_account_user_sync();
22791
22792 if ( $this->_user->is_verified() ) {
22793 return;
22794 }
22795
22796 $api = $this->get_api_site_scope();
22797 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
22798 'after_email_confirm_url' => $this->_get_admin_page_url(
22799 'account',
22800 array( 'fs_action' => 'sync_user' )
22801 )
22802 ) );
22803
22804 if ( ! isset( $result->error ) ) {
22805 $this->_admin_notices->add( sprintf(
22806 $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' ),
22807 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
22808 ) );
22809 } else {
22810 // handle different error cases.
22811
22812 }
22813 }
22814
22815 /**
22816 * @author Vova Feldman (@svovaf)
22817 * @since 1.1.2
22818 *
22819 * @param array $params
22820 * @param bool|null $network
22821 *
22822 * @return string
22823 */
22824 function get_activation_url( $params = array(), $network = null ) {
22825 if ( $this->is_addon() && $this->has_free_plan() ) {
22826 /**
22827 * @author Vova Feldman (@svovaf)
22828 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
22829 */
22830 return $this->get_parent_instance()->get_activation_url( $params );
22831 }
22832
22833 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
22834 }
22835
22836 /**
22837 * @author Vova Feldman (@svovaf)
22838 * @since 1.2.1.5
22839 *
22840 * @param array $params
22841 *
22842 * @return string
22843 */
22844 function get_reconnect_url( $params = array() ) {
22845 $params['fs_action'] = 'reset_anonymous_mode';
22846 $params['fs_unique_affix'] = $this->get_unique_affix();
22847
22848 return $this->get_activation_url( $params );
22849 }
22850
22851 /**
22852 * Get the URL of the page that should be loaded after the user connect
22853 * or skip in the opt-in screen.
22854 *
22855 * @author Vova Feldman (@svovaf)
22856 * @since 1.1.3
22857 *
22858 * @param string $filter Filter name.
22859 * @param array $params Since 1.2.2.7
22860 * @param bool|null $network
22861 *
22862 * @return string
22863 */
22864 function get_after_activation_url( $filter, $params = array(), $network = null ) {
22865 if ( $this->show_opt_in_on_themes_page() &&
22866 ( fs_request_has( 'pending_activation' ) ||
22867 // For cases when the first time path is set, even though it's a WP.org theme.
22868 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
22869 ) {
22870 $first_time_path = '';
22871 } else {
22872 $first_time_path = $this->_menu->get_first_time_path(
22873 fs_is_network_admin() && $this->_is_network_active
22874 );
22875 }
22876
22877 if ( $this->_is_network_active &&
22878 fs_is_network_admin() &&
22879 ! $this->_menu->has_network_menu() &&
22880 $this->is_network_registered()
22881 ) {
22882 $target_url = $this->get_account_url();
22883 } else {
22884 // Default plugin's page.
22885 $target_url = $this->_get_admin_page_url( '', array(), $network );
22886 }
22887
22888 return add_query_arg( $params, $this->apply_filters(
22889 $filter,
22890 empty( $first_time_path ) ?
22891 $target_url :
22892 $first_time_path
22893 ) );
22894 }
22895
22896 /**
22897 * Handle account page updates / edits / actions.
22898 *
22899 * @author Vova Feldman (@svovaf)
22900 * @since 1.0.2
22901 *
22902 */
22903 private function _handle_account_edits() {
22904 if ( ! $this->is_user_admin() ) {
22905 return;
22906 }
22907
22908 $action = fs_get_action();
22909
22910 if ( empty( $action ) ) {
22911 return;
22912 }
22913
22914 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22915 $install_id = fs_request_get( 'install_id', '' );
22916
22917 // Alias.
22918 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22919
22920 $is_network_action = $this->is_network_level_action();
22921 $blog_id = $this->is_network_level_site_specific_action();
22922 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
22923
22924 if ( is_numeric( $blog_id ) ) {
22925 $this->switch_to_blog( $blog_id );
22926 } else {
22927 $blog_id = '';
22928 }
22929
22930 switch ( $action ) {
22931 case 'opt_in':
22932 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22933
22934 if ( $is_parent_plugin_action ) {
22935 if ( $is_network_action && ! empty( $blog_id ) ) {
22936 if ( ! $this->is_registered() ) {
22937 $this->install_with_user(
22938 $this->get_network_user(),
22939 false,
22940 false,
22941 false,
22942 false
22943 );
22944
22945 $this->_admin_notices->add(
22946 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
22947 $this->get_text_inline( 'Awesome', 'awesome' )
22948 );
22949 }
22950 }
22951 }
22952 break;
22953
22954 case 'toggle_tracking':
22955 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22956
22957 if ( $is_parent_plugin_action ) {
22958 if ( $is_network_action && ! empty( $blog_id ) ) {
22959 if ( $this->is_registered( true ) ) {
22960 if ( $this->is_tracking_prohibited( $blog_id ) ) {
22961 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
22962 $this->_admin_notices->add(
22963 sprintf( $this->get_text_inline( 'Sharing diagnostic data with %s helps to provide functionality that\'s more relevant to your website, avoid WordPress or PHP version incompatibilities that can break your website, and recognize which languages & regions the plugin should be translated and tailored to.', 'opt-out-message-appreciation' ), "<b>{$this->get_plugin_title()}</b>" ),
22964 $this->get_text_inline( 'Thank you!', 'thank-you' )
22965 );
22966 }
22967 } else {
22968 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
22969 $install = $this->get_install_by_blog_id( $blog_id );
22970
22971 $this->_admin_notices->add(
22972 sprintf(
22973 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
22974 self::get_unfiltered_site_url( $blog_id, true ),
22975 "<b>{$this->get_plugin_title()}</b>"
22976 )
22977 );
22978 }
22979 }
22980 }
22981 }
22982 }
22983
22984 break;
22985
22986 case 'delete_account':
22987 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
22988
22989 $is_network_deletion = $is_network_action && empty( $blog_id );
22990
22991 if ( $is_parent_plugin_action ) {
22992 // Delete add-on installs if have any.
22993 $installed_addons = $this->get_installed_addons();
22994 foreach ( $installed_addons as $fs_addon ) {
22995 if ( $is_network_deletion ) {
22996 $fs_addon->delete_network_account_event();
22997 } else {
22998 $fs_addon->delete_account_event();
22999 }
23000 }
23001
23002 if ( $is_network_deletion ) {
23003 $this->delete_network_account_event();
23004 } else {
23005 $this->delete_account_event();
23006 }
23007
23008 // Clear user and site.
23009 $this->_site = null;
23010 $this->_user = null;
23011
23012 $this->maybe_set_slug_and_network_menu_exists_flag();
23013
23014 fs_redirect( $this->get_activation_url() );
23015 } else {
23016 if ( $this->is_addon_activated( $plugin_id ) ) {
23017 $fs_addon = self::get_instance_by_id( $plugin_id );
23018
23019 if ( $is_network_deletion ) {
23020 $fs_addon->delete_network_account_event();
23021 } else {
23022 $fs_addon->delete_account_event();
23023 }
23024
23025 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23026 }
23027 }
23028
23029 return;
23030
23031 case 'downgrade_account':
23032 if ( is_numeric( $blog_id ) ) {
23033 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23034 } else {
23035 check_admin_referer( $action );
23036 }
23037
23038 $switch_to_network_install_blog_after_cancellation = (
23039 is_numeric( $blog_id ) &&
23040 $plugin_id == $this->get_id() &&
23041 ! $this->is_trial()
23042 );
23043
23044 $result = $this->cancel_subscription_or_trial( $plugin_id );
23045 if ( $this->is_api_error( $result ) ) {
23046 $this->_admin_notices->add(
23047 $result->error->message,
23048 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23049 'error'
23050 );
23051 }
23052
23053 if ( $switch_to_network_install_blog_after_cancellation ) {
23054 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23055 }
23056
23057 return;
23058
23059 case 'activate_license':
23060 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23061
23062 $fs = $this;
23063 if ( $plugin_id != $this->get_id() ) {
23064 $fs = $this->is_addon_activated( $plugin_id ) ?
23065 self::get_instance_by_id( $plugin_id ) :
23066 null;
23067 }
23068
23069 if ( is_object( $fs ) ) {
23070 $fs->_activate_license();
23071
23072 /**
23073 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23074 *
23075 * @author Leo Fajardo (@leorw)
23076 * @since 2.4.0
23077 */
23078 unset( $_REQUEST['plugin_id'] );
23079
23080 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23081 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23082 }
23083 }
23084
23085 return;
23086
23087 case 'deactivate_license':
23088 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23089
23090 if ( $plugin_id == $this->get_id() ) {
23091 $this->_deactivate_license();
23092
23093 if ( $this->is_only_premium() ) {
23094 // Clear user and site.
23095 $this->_site = null;
23096 $this->_user = null;
23097
23098 if ( ! $is_network_action ) {
23099 fs_redirect( $this->get_activation_url() );
23100 } else if ( is_numeric( $blog_id ) ) {
23101 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23102 }
23103 }
23104 } else {
23105 if ( $this->is_addon_activated( $plugin_id ) ) {
23106 $fs_addon = self::get_instance_by_id( $plugin_id );
23107 $fs_addon->_deactivate_license();
23108 }
23109 }
23110
23111 return;
23112
23113 case 'check_updates':
23114 check_admin_referer( $action );
23115 $this->check_updates();
23116
23117 return;
23118
23119 case 'change_owner':
23120 $state = fs_request_get( 'state', 'init' );
23121 switch ( $state ) {
23122 case 'init':
23123 // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function.
23124 check_admin_referer( 'change_owner' );
23125
23126 $candidate_email = fs_request_get( 'candidate_email' );
23127 $transfer_type = fs_request_get( 'transfer_type' );
23128
23129 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23130 if ( 'transfer' === $transfer_type ) {
23131 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. The email owner must confirm the update within the next 4 hours.', 'change-owner-request-sent-x-transfer' ), '<b>' . $this->_user->email . '</b>' ) );
23132 } else {
23133 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. You must confirm the update within the next 4 hours. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '<b>' . $this->_user->email . '</b>' ) );
23134 }
23135 }
23136 break;
23137 case 'owner_confirmed':
23138 // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API.
23139 $candidate_email = fs_request_get( 'candidate_email', '' );
23140
23141 if ( ! is_email($candidate_email ) ) {
23142 return;
23143 }
23144
23145 $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>' ) );
23146 break;
23147 case 'candidate_confirmed':
23148 // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls.
23149 if ( $this->complete_change_owner() ) {
23150 $this->_admin_notices->add_sticky(
23151 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23152 'ownership_changed',
23153 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23154 );
23155 } else {
23156 // @todo Handle failed ownership change message.
23157 }
23158 break;
23159 }
23160
23161 return;
23162
23163 case 'update_user_name':
23164 check_admin_referer( 'update_user_name' );
23165
23166 $result = $this->update_user_name();
23167
23168 if ( isset( $result->error ) ) {
23169 $this->_admin_notices->add(
23170 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23171 $oops_text,
23172 'error'
23173 );
23174 } else {
23175 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23176 }
23177
23178 return;
23179
23180 #region Actions that might be called from external links (e.g. email)
23181
23182 /**
23183 * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails.
23184 */
23185
23186 case 'cancel_trial':
23187 $result = $this->cancel_subscription_or_trial( $plugin_id );
23188 if ( $this->is_api_error( $result ) ) {
23189 $this->_admin_notices->add(
23190 $result->error->message,
23191 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23192 'error'
23193 );
23194 }
23195
23196 return;
23197
23198 case 'verify_email':
23199 $this->verify_email();
23200
23201 return;
23202
23203 case 'sync_user':
23204 $this->_handle_account_user_sync();
23205
23206 return;
23207
23208 case $this->get_unique_affix() . '_sync_license':
23209 $this->_sync_license();
23210
23211 return;
23212
23213 case 'download_latest':
23214 $this->download_latest_directly( $plugin_id );
23215
23216 return;
23217
23218 #endregion
23219 }
23220
23221 if ( WP_FS__IS_POST_REQUEST ) {
23222 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23223 foreach ( $properties as $p ) {
23224 if ( 'update_' . $p === $action ) {
23225 check_admin_referer( $action );
23226
23227 $this->_logger->log( $action );
23228
23229 $site_property = substr( $p, strlen( 'site_' ) );
23230 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23231 $this->get_site()->{$site_property} = $site_property_value;
23232
23233 // Store account after modification.
23234 $this->_store_site();
23235
23236 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23237
23238 $this->_admin_notices->add( sprintf(
23239 /* translators: %s: User's account property (e.g. email address, name) */
23240 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23241 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23242 ) );
23243
23244 return;
23245 }
23246 }
23247 }
23248 }
23249
23250 /**
23251 * Adds CSS classes for the body tag in the admin.
23252 *
23253 * @param string $classes Space-separated string of class names.
23254 *
23255 * @return string $classes FS Admin body tag class names.
23256 */
23257 public function fs_addons_body_class( $classes ) {
23258 $classes .= ' plugins-php';
23259 return $classes;
23260 }
23261
23262 /**
23263 * Account page resources load.
23264 *
23265 * @author Vova Feldman (@svovaf)
23266 * @since 1.0.6
23267 */
23268 function _account_page_load() {
23269 $this->_logger->entrance();
23270
23271 $this->_logger->info( var_export( $_REQUEST, true ) );
23272
23273 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23274
23275 if ( $this->has_addons() ) {
23276 wp_enqueue_script( 'plugin-install' );
23277 add_thickbox();
23278 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23279 }
23280
23281 if ( $this->has_paid_plan() &&
23282 ! $this->has_any_license() &&
23283 ! $this->is_sync_executed() &&
23284 $this->is_tracking_allowed()
23285 ) {
23286 /**
23287 * If no licenses found and no sync job was executed during the last 24 hours,
23288 * just execute the sync job right away (blocking execution).
23289 *
23290 * @since 1.1.7.3
23291 */
23292 $this->run_manual_sync();
23293 }
23294
23295 $this->_handle_account_edits();
23296
23297 if (
23298 is_object( $this->_license ) &&
23299 $this->_license->user_id == $this->_user->id &&
23300 ! $this->is_whitelabeled( true )
23301 ) {
23302 $this->_admin_notices->add(
23303 sprintf(
23304 $this->get_text_inline( "Is this your client's site? %s if you wish to hide sensitive info like your email, license key, prices, billing address & invoices from the WP Admin.", 'license_not_whitelabeled' ),
23305 sprintf(
23306 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23307 $this->get_text_inline( 'Click here', 'click-here' )
23308 )
23309 ),
23310 '',
23311 'success',
23312 false,
23313 'license_not_whitelabeled'
23314 );
23315 }
23316
23317 $this->do_action( 'account_page_load_before_departure' );
23318 }
23319
23320 /**
23321 * Renders the "Affiliation" page.
23322 *
23323 * @author Leo Fajardo (@leorw)
23324 * @since 1.2.3
23325 */
23326 function _affiliation_page_render() {
23327 $this->_logger->entrance();
23328
23329 $this->fetch_affiliate_and_terms();
23330
23331 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23332
23333 $is_bundle_context = $this->has_bundle_context();
23334
23335 $plugin_title = $this->get_plugin_title();
23336
23337 if ( $is_bundle_context ) {
23338 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23339
23340 // Add the suffix "Bundle" only if the word is not present in the title itself.
23341 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23342 $plugin_title = $this->apply_filters(
23343 'formatted_bundle_title',
23344 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23345 );
23346 }
23347 }
23348
23349 $vars = array(
23350 'id' => $this->_module_id,
23351 'plugin_title' => $plugin_title,
23352 );
23353 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23354 }
23355
23356
23357 /**
23358 * Render account page.
23359 *
23360 * @author Vova Feldman (@svovaf)
23361 * @since 1.0.0
23362 */
23363 function _account_page_render() {
23364 $this->_logger->entrance();
23365
23366 $template = 'account.php';
23367 $vars = array( 'id' => $this->_module_id );
23368
23369 /**
23370 * Added filter to the template to allow developers wrapping the template
23371 * in custom HTML (e.g. within a wizard/tabs).
23372 *
23373 * @author Vova Feldman (@svovaf)
23374 * @since 1.2.1.6
23375 */
23376 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23377 }
23378
23379 /**
23380 * Render account connect page.
23381 *
23382 * @author Vova Feldman (@svovaf)
23383 * @since 1.0.7
23384 */
23385 function _connect_page_render() {
23386 $this->_logger->entrance();
23387
23388 $vars = array( 'id' => $this->_module_id );
23389
23390 /**
23391 * Added filter to the template to allow developers wrapping the template
23392 * in custom HTML (e.g. within a wizard/tabs).
23393 *
23394 * @author Vova Feldman (@svovaf)
23395 * @since 1.2.1.6
23396 */
23397 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23398 }
23399
23400 /**
23401 * Load required resources before add-ons page render.
23402 *
23403 * @author Vova Feldman (@svovaf)
23404 * @since 1.0.6
23405 */
23406 function _addons_page_load() {
23407 $this->_logger->entrance();
23408
23409 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23410
23411 wp_enqueue_script( 'plugin-install' );
23412 add_thickbox();
23413 add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) );
23414
23415 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23416 $this->_admin_notices->add(
23417 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>' ),
23418 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23419 'update-nag'
23420 );
23421 }
23422 }
23423
23424 /**
23425 * Render add-ons page.
23426 *
23427 * @author Vova Feldman (@svovaf)
23428 * @since 1.0.6
23429 */
23430 function _addons_page_render() {
23431 $this->_logger->entrance();
23432
23433 $vars = array( 'id' => $this->_module_id );
23434
23435 /**
23436 * Added filter to the template to allow developers wrapping the template
23437 * in custom HTML (e.g. within a wizard/tabs).
23438 *
23439 * @author Vova Feldman (@svovaf)
23440 * @since 1.2.1.6
23441 */
23442 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23443 }
23444
23445 /* Pricing & Upgrade
23446 ------------------------------------------------------------------------------------------------------------------*/
23447 /**
23448 * Render pricing page.
23449 *
23450 * @author Vova Feldman (@svovaf)
23451 * @since 1.0.0
23452 */
23453 function _pricing_page_render() {
23454 $this->_logger->entrance();
23455
23456 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
23457 fs_enqueue_local_style( 'fs_checkout', '/admin/checkout.css' );
23458
23459 $vars = array( 'id' => $this->_module_id );
23460
23461 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23462 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23463 } else {
23464 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23465 }
23466 }
23467
23468 /**
23469 * @author Leo Fajardo (@leorw)
23470 * @since 2.3.1
23471 */
23472 function _add_pricing_ajax_handler() {
23473 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23474 }
23475
23476 /**
23477 * @author Leo Fajardo (@leorw)
23478 * @since 2.3.1
23479 */
23480 function _fs_pricing_ajax_action_handler() {
23481 $this->check_ajax_referer( 'pricing_ajax_action' );
23482
23483 $result = null;
23484 $pricing_action = fs_request_get( 'pricing_action' );
23485
23486 switch ( $pricing_action ) {
23487 case 'fetch_pricing_data':
23488 $params = array(
23489 'is_enriched' => true,
23490 'trial' => fs_request_get_bool( 'trial' ),
23491 'sandbox' => fs_request_get_raw( 'sandbox' ),
23492 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ),
23493 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ),
23494 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ),
23495 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ),
23496 );
23497
23498 $bundle_id = $this->get_bundle_id();
23499 $bundle_public_key = $this->get_bundle_public_key();
23500
23501 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23502
23503 if ( ! $has_bundle_context ) {
23504 $api = $this->get_api_plugin_scope();
23505 } else {
23506 $api = FS_Api::instance(
23507 $bundle_id,
23508 'plugin',
23509 $bundle_id,
23510 $bundle_public_key,
23511 ! $this->is_live(),
23512 false,
23513 $this->get_sdk_version()
23514 );
23515
23516 $params['plugin_id'] = $this->get_id();
23517 $params['plugin_public_key'] = $this->get_public_key();
23518 }
23519
23520 $result = $api->get( $this->add_show_pending( 'pricing.json?' . http_build_query( $params ) ) );
23521 break;
23522 case 'start_trial':
23523 $trial_plan_id = fs_request_get( 'plan_id' );
23524
23525 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
23526 $plan = $this->_get_plan_by_id( $trial_plan_id );
23527
23528 if ( ! $plan ) {
23529 $this->shoot_ajax_failure( 'Invalid plan ID.' );
23530 return;
23531 }
23532
23533 $result = $this->start_trial( $plan->name, true );
23534 } else {
23535 // @todo - This fails for sandbox trial at the moment if the trial was already utilized.
23536 $result = $this->opt_in(
23537 false,
23538 false,
23539 false,
23540 false,
23541 false,
23542 $trial_plan_id
23543 );
23544 }
23545 }
23546
23547 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23548 $this->_logger->api_error( $result );
23549
23550 self::shoot_ajax_failure(
23551 isset( $result->error ) ?
23552 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23553 var_export( $result, true )
23554 );
23555 }
23556
23557 $this->shoot_ajax_success( $result );
23558 }
23559
23560 #----------------------------------------------------------------------------------
23561 #region Contact Us
23562 #----------------------------------------------------------------------------------
23563
23564 /**
23565 * Render contact-us page.
23566 *
23567 * @author Vova Feldman (@svovaf)
23568 * @since 1.0.3
23569 */
23570 function _contact_page_render() {
23571 $this->_logger->entrance();
23572
23573 $vars = array( 'id' => $this->_module_id );
23574
23575 /**
23576 * Added filter to the template to allow developers wrapping the template
23577 * in custom HTML (e.g. within a wizard/tabs).
23578 *
23579 * @author Vova Feldman (@svovaf)
23580 * @since 2.1.3
23581 */
23582 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23583 }
23584
23585 #endregion ------------------------------------------------------------------------
23586
23587 /**
23588 * Hide all admin notices to prevent distractions.
23589 *
23590 * @author Vova Feldman (@svovaf)
23591 * @since 1.0.3
23592 *
23593 * @uses remove_all_actions()
23594 */
23595 private static function _hide_admin_notices() {
23596 remove_all_actions( 'admin_notices' );
23597 remove_all_actions( 'network_admin_notices' );
23598 remove_all_actions( 'all_admin_notices' );
23599 remove_all_actions( 'user_admin_notices' );
23600 }
23601
23602 static function _clean_admin_content_section_hook() {
23603 $hide_admin_notices = true;
23604
23605 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23606 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23607
23608 $hide_admin_notices = false;
23609 }
23610
23611 if ( $hide_admin_notices ) {
23612 self::_hide_admin_notices();
23613 }
23614
23615 // Hide footer.
23616 echo '<style>#wpfooter { display: none !important; }</style>';
23617 }
23618
23619 /**
23620 * Attach to admin_head hook to hide all admin notices.
23621 *
23622 * @author Vova Feldman (@svovaf)
23623 * @since 1.0.3
23624 */
23625 static function _clean_admin_content_section() {
23626 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23627 }
23628
23629 /* CSS & JavaScript
23630 ------------------------------------------------------------------------------------------------------------------*/
23631 /* function _enqueue_script($handle, $src) {
23632 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23633
23634 $this->_logger->entrance( 'script = ' . $url );
23635
23636 wp_enqueue_script( $handle, $url );
23637 }*/
23638
23639 /* SDK
23640 ------------------------------------------------------------------------------------------------------------------*/
23641 private $_user_api;
23642
23643 /**
23644 *
23645 * @author Vova Feldman (@svovaf)
23646 * @since 1.0.2
23647 *
23648 * @param bool $flush
23649 *
23650 * @return FS_Api
23651 */
23652 function get_api_user_scope( $flush = false ) {
23653 if ( ! isset( $this->_user_api ) || $flush ) {
23654 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23655 }
23656
23657 return $this->_user_api;
23658 }
23659
23660 /**
23661 * @author Vova Feldman (@svovaf)
23662 * @since 2.0.0
23663 *
23664 * @param \FS_User $user
23665 *
23666 * @return \FS_Api
23667 */
23668 private function get_api_user_scope_by_user( FS_User $user ) {
23669 return FS_Api::instance(
23670 $this->_module_id,
23671 'user',
23672 $user->id,
23673 $user->public_key,
23674 ! $this->is_live(),
23675 $user->secret_key,
23676 $this->get_sdk_version()
23677 );
23678 }
23679
23680 /**
23681 *
23682 * @author Leo Fajardo (@leorw)
23683 * @since 2.0.0
23684 *
23685 * @param bool $flush
23686 *
23687 * @return FS_Api
23688 */
23689 private function get_current_or_network_user_api_scope( $flush = false ) {
23690 if ( ! $this->_is_network_active ||
23691 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23692 ) {
23693 return $this->get_api_user_scope( $flush );
23694 }
23695
23696 $user = $this->get_current_or_network_user();
23697
23698 $this->_user_api = FS_Api::instance(
23699 $this->_module_id,
23700 'user',
23701 $user->id,
23702 $user->public_key,
23703 ! $this->is_live(),
23704 $user->secret_key,
23705 $this->get_sdk_version()
23706 );
23707
23708 return $this->_user_api;
23709 }
23710
23711 private $_site_api;
23712
23713 /**
23714 *
23715 * @author Vova Feldman (@svovaf)
23716 * @since 1.0.2
23717 *
23718 * @param bool $flush
23719 *
23720 * @return FS_Api
23721 */
23722 private function get_api_site_scope( $flush = false ) {
23723 if ( ! isset( $this->_site_api ) || $flush ) {
23724 $this->_site_api = FS_Api::instance(
23725 $this->_module_id,
23726 'install',
23727 $this->_site->id,
23728 $this->_site->public_key,
23729 ! $this->is_live(),
23730 $this->_site->secret_key,
23731 $this->get_sdk_version(),
23732 self::get_unfiltered_site_url()
23733 );
23734 }
23735
23736 return $this->_site_api;
23737 }
23738
23739 /**
23740 * @author Leo Fajardo (@leorw)
23741 * @since 2.5.0
23742 *
23743 * @param string $path
23744 * @param string $method
23745 * @param array $params
23746 * @param bool $flush_instance
23747 *
23748 * @return array|mixed|string|void
23749 * @throws Freemius_Exception
23750 */
23751 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23752 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23753
23754 /**
23755 * Checks if the local install's URL is different from the remote install's URL, update the local install if necessary, and then run the clone handler if the install's URL is different from the URL of the site.
23756 *
23757 * @author Leo Fajardo (@leorw)
23758 * @since 2.5.0
23759 */
23760 if (
23761 $this->is_registered() &&
23762 FS_Api::is_api_result_entity( $result ) &&
23763 isset( $result->url )
23764 ) {
23765 $stored_local_url = trailingslashit( $this->_site->url );
23766 $stored_remote_url = trailingslashit( $result->url );
23767
23768 if ( $stored_local_url !== $stored_remote_url ) {
23769 $this->_site->url = $result->url;
23770 $this->_store_site();
23771 }
23772
23773 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
23774 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
23775 }
23776 }
23777
23778 return $result;
23779 }
23780
23781 private $_plugin_api;
23782
23783 /**
23784 * Get plugin public API scope.
23785 *
23786 * @author Vova Feldman (@svovaf)
23787 * @since 1.0.7
23788 *
23789 * @return FS_Api
23790 */
23791 function get_api_plugin_scope() {
23792 if ( ! isset( $this->_plugin_api ) ) {
23793 $this->_plugin_api = FS_Api::instance(
23794 $this->_module_id,
23795 'plugin',
23796 $this->_plugin->id,
23797 $this->_plugin->public_key,
23798 ! $this->is_live(),
23799 false,
23800 $this->get_sdk_version()
23801 );
23802 }
23803
23804 return $this->_plugin_api;
23805 }
23806
23807 /**
23808 * Get bundle public API scope.
23809 *
23810 * @author Vova Feldman (@svovaf)
23811 * @since 2.3.1
23812 *
23813 * @return FS_Api
23814 */
23815 function get_api_bundle_scope() {
23816 return FS_Api::instance(
23817 $this->get_bundle_id(),
23818 'plugin',
23819 $this->get_bundle_id(),
23820 $this->get_bundle_public_key(),
23821 ! $this->is_live(),
23822 false,
23823 $this->get_sdk_version()
23824 );
23825 }
23826
23827 /**
23828 * Get site API scope object (fallback to public plugin scope when not registered).
23829 *
23830 * @author Vova Feldman (@svovaf)
23831 * @since 1.0.7
23832 *
23833 * @return FS_Api
23834 */
23835 function get_api_site_or_plugin_scope() {
23836 return $this->is_registered() ?
23837 $this->get_api_site_scope() :
23838 $this->get_api_plugin_scope();
23839 }
23840
23841 /**
23842 * @author Leo Fajardo (@leorw)
23843 * @since 2.2.3.1
23844 *
23845 * @param object $result
23846 */
23847 private function maybe_modify_api_curl_error_message( $result ) {
23848 if (
23849 'cUrlMissing' !== $result->error->type &&
23850 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
23851 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
23852 ) {
23853 return;
23854 }
23855
23856 $result->error->message = $this->esc_html_inline( 'We use PHP cURL library for the API calls, which is a very common library and usually installed and activated out of the box. Unfortunately, cURL is not activated (or disabled) on your server.', 'curl-missing-message' ) .
23857 ' ' .
23858 $this->esc_html_inline(
23859 sprintf(
23860 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
23861 implode(
23862 ', ',
23863 $this->apply_filters( 'api_domains', array(
23864 'api.freemius.com',
23865 'wp.freemius.com'
23866 ) )
23867 )
23868 ),
23869 'connectivity-whitelist'
23870 ) .
23871 ' ' .
23872 sprintf(
23873 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
23874 $this->get_module_type()
23875 );
23876 }
23877
23878 /**
23879 * Show trial promotional notice (if any trial exist).
23880 *
23881 * @author Vova Feldman (@svovaf)
23882 * @since 1.0.9
23883 *
23884 * @param FS_Plugin_Plan[] $plans
23885 */
23886 function _check_for_trial_plans( $plans ) {
23887 /**
23888 * 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.
23889 *
23890 * @author Vova Feldman (@svovaf)
23891 * @since 2.1.2
23892 */
23893 if ( ! is_array( $plans ) && is_object( $plans ) ) {
23894 $plans = array( $plans );
23895 }
23896
23897 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
23898 $plans = array();
23899 }
23900
23901 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
23902 }
23903
23904 /**
23905 * During trial promotion the "upgrade" submenu item turns to
23906 * "start trial" to encourage the trial. Since we want to keep
23907 * the same menu item handler and there's no robust way to
23908 * add new arguments to the menu item link's querystring,
23909 * use JavaScript to find the menu item and update the href of
23910 * the link.
23911 *
23912 * @author Vova Feldman (@svovaf)
23913 * @since 1.2.1.5
23914 */
23915 function _fix_start_trial_menu_item_url() {
23916 $template_args = array( 'id' => $this->_module_id );
23917 fs_require_template( 'add-trial-to-pricing.php', $template_args );
23918 }
23919
23920 /**
23921 * Check if module is currently in a trial promotion mode.
23922 *
23923 * @author Vova Feldman (@svovaf)
23924 * @since 1.2.2.7
23925 *
23926 * @return bool
23927 */
23928 function is_in_trial_promotion() {
23929 return $this->_admin_notices->has_sticky( 'trial_promotion' );
23930 }
23931
23932 /**
23933 * Show trial promotional notice (if any trial exist).
23934 *
23935 * @author Vova Feldman (@svovaf)
23936 * @since 1.0.9
23937 *
23938 * @return bool If trial notice added.
23939 */
23940 function _add_trial_notice() {
23941 if ( ! $this->is_user_admin() ) {
23942 return false;
23943 }
23944
23945 if ( ! $this->is_user_in_admin() ) {
23946 return false;
23947 }
23948
23949 if ( $this->_is_network_active ) {
23950 if ( fs_is_network_admin() ) {
23951 // Network level trial is disabled at the moment.
23952 return false;
23953 }
23954
23955 if ( ! $this->is_delegated_connection() ) {
23956 // Only delegated sites should support trials.
23957 return false;
23958 }
23959 }
23960
23961 // Check if trial message is already shown.
23962 if ( $this->is_in_trial_promotion() ) {
23963 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
23964
23965 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
23966
23967 return false;
23968 }
23969
23970 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
23971 // Don't show trial if running the premium code, unless running in DEV mode.
23972 return false;
23973 }
23974
23975 if ( ! $this->has_trial_plan() ) {
23976 // No plans with trial.
23977 return false;
23978 }
23979
23980 if ( ! $this->apply_filters( 'show_trial', true ) ) {
23981 // Developer explicitly asked not to show the trial promo.
23982 return false;
23983 }
23984
23985 if ( $this->is_registered() ) {
23986 // Check if trial already utilized.
23987 if ( $this->_site->is_trial_utilized() ) {
23988 return false;
23989 }
23990
23991 if ( $this->is_paying_or_trial() ) {
23992 // Don't show trial if paying or already in trial.
23993 return false;
23994 }
23995 }
23996
23997 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
23998 // If not yet opted-in/skipped, or pending activation, don't show trial.
23999 return false;
24000 }
24001
24002 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24003 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24004
24005 // Show promotion if never shown before and 24 hours after initial activation with FS.
24006 if ( ! $was_promotion_shown_before &&
24007 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24008 ) {
24009 return false;
24010 }
24011
24012 // OR if promotion was shown before, try showing it every 30 days.
24013 if ( $was_promotion_shown_before &&
24014 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24015 ) {
24016 return false;
24017 }
24018
24019 $trial_period = $this->_trial_days;
24020 $require_payment = $this->_is_trial_require_payment;
24021 $trial_url = $this->get_trial_url();
24022 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24023
24024 if ( $this->is_registered() ) {
24025 // If opted-in, override trial with up to date data from API.
24026 $trial_plans = FS_Plan_Manager::instance()->get_visible_trial_plans( $this->_plans );
24027 $trial_plans_count = count( $trial_plans );
24028
24029 if ( 0 === $trial_plans_count ) {
24030 // If there's no plans with a trial just exit.
24031 return false;
24032 }
24033
24034 /**
24035 * @var FS_Plugin_Plan $paid_plan
24036 */
24037 $paid_plan = $trial_plans[0];
24038 $require_payment = $paid_plan->is_require_subscription;
24039 $trial_period = $paid_plan->trial_period;
24040
24041 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24042
24043 if ( $total_paid_plans !== $trial_plans_count ) {
24044 // Not all paid plans have a trial - generate a string of those that have it.
24045 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24046 $plans_string .= sprintf(
24047 ' <a href="%s">%s</a>',
24048 $trial_url,
24049 $trial_plans[ $i ]->title
24050 );
24051
24052 if ( $i < $trial_plans_count - 2 ) {
24053 $plans_string .= ', ';
24054 } else if ( $i == $trial_plans_count - 2 ) {
24055 $plans_string .= ' and ';
24056 }
24057 }
24058 }
24059 }
24060
24061 $message = sprintf(
24062 $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' ),
24063 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24064 $plans_string,
24065 $trial_period
24066 );
24067
24068 // "No Credit-Card Required" or "No Commitment for N Days".
24069 $cc_string = $require_payment ?
24070 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24071 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24072
24073
24074 // Start trial button.
24075 $button = ' ' . sprintf(
24076 '<div><a class="button button-primary" href="%s">%s &nbsp;&#10140;</a></div>',
24077 $trial_url,
24078 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24079 );
24080
24081 $message_text = $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string}" );
24082
24083 $this->_admin_notices->add_sticky(
24084 "<div class=\"fs-trial-message-container\"><div>{$message_text}</div> {$button}</div>",
24085 'trial_promotion',
24086 '',
24087 'promotion'
24088 );
24089
24090 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24091
24092 return true;
24093 }
24094
24095 /**
24096 * Lets users/customers know that the product has an affiliate program.
24097 *
24098 * @author Leo Fajardo (@leorw)
24099 * @since 1.2.2.11
24100 *
24101 * @return bool Returns true if the notice has been added.
24102 */
24103 function _add_affiliate_program_notice() {
24104 if ( ! $this->is_user_admin() ) {
24105 return false;
24106 }
24107
24108 if ( ! $this->is_user_in_admin() ) {
24109 return false;
24110 }
24111
24112 // Check if the notice is already shown.
24113 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24114 return false;
24115 }
24116
24117 if (
24118 // Product has no affiliate program.
24119 ! $this->has_affiliate_program() ||
24120 // User has applied for an affiliate account.
24121 ! empty( $this->_storage->affiliate_application_data )
24122 ) {
24123 return false;
24124 }
24125
24126 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24127 // Developer explicitly asked not to show the notice about the affiliate program.
24128 return false;
24129 }
24130
24131 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24132 // If not yet opted in/skipped, or pending activation, don't show the notice.
24133 return false;
24134 }
24135
24136 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24137 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24138
24139 /**
24140 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24141 * activation with FS.
24142 */
24143 if ( $was_notice_shown_before ||
24144 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24145 ) {
24146 return false;
24147 }
24148
24149 if ( ! $this->is_paying() &&
24150 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24151 ) {
24152 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24153 return false;
24154 }
24155
24156 $message = sprintf(
24157 $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' ),
24158 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24159 $this->get_module_label( true )
24160 );
24161
24162 // HTML code for the "Learn more..." button.
24163 $button = ' ' . sprintf(
24164 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24165 $this->_get_admin_page_url( 'affiliation' ),
24166 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24167 );
24168
24169 $this->_admin_notices->add_sticky(
24170 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24171 'affiliate_program',
24172 '',
24173 'promotion'
24174 );
24175
24176 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24177
24178 return true;
24179 }
24180
24181 /**
24182 * @author Vova Feldman (@svovaf)
24183 * @since 1.2.1.5
24184 */
24185 function _enqueue_common_css() {
24186 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24187 // Add basic CSS for admin-notices and menu-item colors.
24188 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24189 }
24190 }
24191
24192 /**
24193 * @author Leo Fajardo (@leorw)
24194 * @since 1.2.2
24195 */
24196 function _show_theme_activation_optin_dialog() {
24197 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24198
24199 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24200 }
24201
24202 /**
24203 * @author Leo Fajardo (@leorw)
24204 * @since 1.2.2
24205 */
24206 function _add_fs_theme_activation_dialog() {
24207 global $pagenow;
24208
24209 if ( 'themes.php' !== $pagenow ) {
24210 return;
24211 }
24212
24213 $vars = array( 'id' => $this->_module_id );
24214 fs_require_once_template( 'connect.php', $vars );
24215 }
24216
24217 /* Action Links
24218 ------------------------------------------------------------------------------------------------------------------*/
24219 private $_action_links_hooked = false;
24220 private $_action_links = array();
24221
24222 /**
24223 * Hook to plugin action links filter.
24224 *
24225 * @author Vova Feldman (@svovaf)
24226 * @since 1.0.0
24227 */
24228 private function hook_plugin_action_links() {
24229 $this->_logger->entrance();
24230
24231 $this->_action_links_hooked = true;
24232
24233 $this->_logger->log( 'Adding action links hooks.' );
24234
24235 // Add action link to settings page.
24236 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24237 &$this,
24238 '_modify_plugin_action_links_hook'
24239 ), WP_FS__DEFAULT_PRIORITY, 2 );
24240 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24241 &$this,
24242 '_modify_plugin_action_links_hook'
24243 ), WP_FS__DEFAULT_PRIORITY, 2 );
24244 }
24245
24246 /**
24247 * Add plugin action link.
24248 *
24249 * @author Vova Feldman (@svovaf)
24250 * @since 1.0.0
24251 *
24252 * @param $label
24253 * @param $url
24254 * @param bool $external
24255 * @param int $priority
24256 * @param bool $key
24257 */
24258 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24259 $this->_logger->entrance();
24260
24261 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24262 $this->_action_links[ $priority ] = array();
24263 }
24264
24265 if ( false === $key ) {
24266 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24267 }
24268
24269 $this->_action_links[ $priority ][] = array(
24270 'label' => $label,
24271 'href' => $url,
24272 'key' => $key,
24273 'external' => $external
24274 );
24275 }
24276
24277 /**
24278 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24279 *
24280 * @author Vova Feldman (@svovaf)
24281 * @since 1.0.0
24282 */
24283 function _add_upgrade_action_link() {
24284 $this->_logger->entrance();
24285
24286 $is_activation_mode = $this->is_activation_mode();
24287
24288 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24289
24290 /**
24291 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24292 * and "Add-Ons" menus should be added.
24293 *
24294 * @author Leo Fajardo (@leorw)
24295 * @since 2.3.0
24296 */
24297 $add_upgrade_link = (
24298 $add_action_links ||
24299 ( $is_activation_mode && $this->is_only_premium() )
24300 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24301
24302 $add_addons_link = ( $add_action_links && $this->has_addons() );
24303
24304 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24305 return;
24306 }
24307
24308 if (
24309 $add_upgrade_link &&
24310 $this->is_pricing_page_visible() &&
24311 $this->is_submenu_item_visible( 'pricing' )
24312 ) {
24313 $this->add_plugin_action_link(
24314 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24315 $this->get_upgrade_url(),
24316 false,
24317 7,
24318 'upgrade'
24319 );
24320 }
24321
24322 if (
24323 $add_addons_link &&
24324 $this->has_addons() &&
24325 $this->is_submenu_item_visible( 'addons' )
24326 ) {
24327 $this->add_plugin_action_link(
24328 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24329 $this->_get_admin_page_url( 'addons' ),
24330 false,
24331 9,
24332 'addons'
24333 );
24334 }
24335 }
24336
24337 /**
24338 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24339 *
24340 * @author Leo Fajardo (@leorw)
24341 * @since 1.1.9
24342 */
24343 function _add_license_action_link() {
24344 $this->_logger->entrance();
24345
24346 if ( ! self::is_ajax() ) {
24347 // Inject license activation dialog UI and client side code.
24348 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24349 }
24350
24351 $link_text = $this->is_free_plan() ?
24352 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24353 $this->get_text_inline( 'Change License', 'change-license' );
24354
24355 $this->add_plugin_action_link(
24356 $link_text,
24357 '#',
24358 false,
24359 11,
24360 ( 'activate-license ' . $this->get_unique_affix() )
24361 );
24362 }
24363
24364 /**
24365 * @author Leo Fajardo (@leorw)
24366 * @since 2.0.2
24367 */
24368 function _add_premium_version_upgrade_selection_action() {
24369 $this->_logger->entrance();
24370
24371 if ( ! self::is_ajax() ) {
24372 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24373 }
24374 }
24375
24376 /**
24377 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24378 *
24379 * @author Leo Fajardo (@leorw)
24380 * @since 1.2.1.5
24381 */
24382 function _add_tracking_links() {
24383 if ( ! current_user_can( 'manage_options' ) ) {
24384 return;
24385 }
24386
24387 $this->_logger->entrance();
24388
24389 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24390 // 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.
24391 return;
24392 }
24393
24394 if (
24395 $this->is_addon() &&
24396 ! $this->is_only_premium()
24397 ) {
24398 $parent = $this->get_parent_instance();
24399
24400 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24401 return;
24402 }
24403 }
24404
24405 if ( fs_is_network_admin() ) {
24406 if ( ! $this->_is_network_active ) {
24407 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24408 return;
24409 } else if ( $this->is_network_delegated_connection() ) {
24410 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24411 return;
24412 }
24413 } else {
24414 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24415 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24416 return;
24417 }
24418 }
24419
24420 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24421 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24422 $this->connect_again();
24423
24424 return;
24425 }
24426 }
24427
24428 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24429 ( $this->is_theme() && ! self::is_themes_page() )
24430 ) {
24431 // Only show tracking links on the plugins and themes pages.
24432 return;
24433 }
24434
24435 if (
24436 $this->is_activation_mode() &&
24437 $this->is_premium() &&
24438 ! $this->is_registered()
24439 ) {
24440 // If not yet registered and running the premium code base, a license activation link will already be shown.
24441 return;
24442 }
24443
24444 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24445 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24446 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24447 return;
24448 }
24449 }
24450
24451 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24452 return;
24453 }
24454
24455 $link_text_id = '';
24456 $url = '#';
24457
24458 if ( $this->is_registered( true ) ) {
24459 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24460 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24461 } else {
24462 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24463 }
24464 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24465 /**
24466 * Show opt-in link only if skipped or in activation mode.
24467 */
24468 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24469
24470 $params = ! $this->is_anonymous() ?
24471 array() :
24472 array(
24473 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24474 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24475 );
24476
24477 $url = $this->get_activation_url( $params );
24478 }
24479
24480 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24481
24482 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24483 $this->add_plugin_action_link(
24484 $link_text_id,
24485 $url,
24486 false,
24487 13,
24488 "opt-in-or-opt-out {$this->_slug}"
24489 );
24490 }
24491 }
24492
24493 /**
24494 * Get the URL of the page that should be loaded right after the plugin activation.
24495 *
24496 * @author Vova Feldman (@svovaf)
24497 * @since 1.1.7.4
24498 *
24499 * @return string
24500 */
24501 function get_after_plugin_activation_redirect_url() {
24502 $url = false;
24503
24504 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24505 $first_time_path = $this->_menu->get_first_time_path(
24506 fs_is_network_admin() && $this->_is_network_active
24507 );
24508
24509 if ( $this->is_activation_mode() ) {
24510 $url = $this->get_activation_url();
24511 } else if ( ! empty( $first_time_path ) ) {
24512 $url = $first_time_path;
24513 } else {
24514 $page = '';
24515 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24516 if ( $this->is_network_registered() ) {
24517 $page = 'account';
24518 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24519 $this->maybe_set_slug_and_network_menu_exists_flag();
24520 }
24521 }
24522
24523 $url = $this->_get_admin_page_url( $page );
24524 }
24525 } else {
24526 $plugin_fs = false;
24527
24528 if ( $this->is_parent_plugin_installed() ) {
24529 $plugin_fs = self::get_parent_instance();
24530 }
24531
24532 if ( is_object( $plugin_fs ) ) {
24533 if ( ! $plugin_fs->is_registered() ) {
24534 // Forward to parent plugin connect when parent not registered.
24535 $url = $plugin_fs->get_activation_url();
24536 } else {
24537 // Forward to account page.
24538 $url = $plugin_fs->_get_admin_page_url( 'account' );
24539 }
24540 }
24541 }
24542
24543 return $url;
24544 }
24545
24546 /**
24547 * Forward page to activation page.
24548 *
24549 * @author Vova Feldman (@svovaf)
24550 * @since 1.0.3
24551 */
24552 function _redirect_on_activation_hook() {
24553 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24554 $url = $this->get_after_plugin_activation_redirect_url();
24555
24556 if ( is_string( $url ) ) {
24557 fs_redirect( $url );
24558 }
24559 }
24560 }
24561
24562 /**
24563 * Modify plugin's page action links collection.
24564 *
24565 * @author Vova Feldman (@svovaf)
24566 * @since 1.0.0
24567 *
24568 * @param array $links
24569 * @param $file
24570 *
24571 * @return array
24572 */
24573 function _modify_plugin_action_links_hook( $links, $file ) {
24574 $this->_logger->entrance();
24575
24576 $passed_deactivate = false;
24577 $deactivate_link = '';
24578 $before_deactivate = array();
24579 $after_deactivate = array();
24580 foreach ( $links as $key => $link ) {
24581 if ( 'deactivate' === $key ) {
24582 $deactivate_link = $link;
24583 $passed_deactivate = true;
24584 continue;
24585 }
24586
24587 if ( ! $passed_deactivate ) {
24588 $before_deactivate[ $key ] = $link;
24589 } else {
24590 $after_deactivate[ $key ] = $link;
24591 }
24592 }
24593
24594 ksort( $this->_action_links );
24595
24596 foreach ( $this->_action_links as $new_links ) {
24597 foreach ( $new_links as $link ) {
24598 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24599 }
24600 }
24601
24602 if ( ! empty( $deactivate_link ) ) {
24603 /**
24604 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24605 *
24606 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24607 */
24608 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24609
24610 // Append deactivation link.
24611 $before_deactivate['deactivate'] = $deactivate_link;
24612 }
24613
24614 return array_merge( $before_deactivate, $after_deactivate );
24615 }
24616
24617 /**
24618 * Adds admin message.
24619 *
24620 * @author Vova Feldman (@svovaf)
24621 * @since 1.0.4
24622 *
24623 * @param string $message
24624 * @param string $title
24625 * @param string $type
24626 */
24627 function add_admin_message( $message, $title = '', $type = 'success' ) {
24628 $this->_admin_notices->add( $message, $title, $type );
24629 }
24630
24631 /**
24632 * Adds sticky admin message.
24633 *
24634 * @author Vova Feldman (@svovaf)
24635 * @since 1.1.0
24636 *
24637 * @param string $message
24638 * @param string $id
24639 * @param string $title
24640 * @param string $type
24641 */
24642 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24643 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24644 }
24645
24646 /**
24647 * Check if the paid version of the module is installed.
24648 *
24649 * @author Vova Feldman (@svovaf)
24650 * @since 2.2.0
24651 *
24652 * @return bool
24653 */
24654 private function is_premium_version_installed() {
24655 $premium_plugin_basename = $this->premium_plugin_basename();
24656
24657 if ( $this->is_theme() ) {
24658 return $this->can_activate_theme( $this->get_premium_slug() );
24659 }
24660
24661 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24662 }
24663
24664 /**
24665 * Helper function that returns the final steps for the upgrade completion.
24666 *
24667 * If the module is already running the premium code, returns an empty string.
24668 *
24669 * @author Vova Feldman (@svovaf)
24670 * @since 1.2.1
24671 *
24672 * @param string $plan_title
24673 *
24674 * @return string
24675 */
24676 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24677 $this->_logger->entrance();
24678
24679 $activate_license_string = $this->get_license_network_activation_notice();
24680
24681 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24682 return '' . $activate_license_string;
24683 }
24684
24685 if ( empty( $plan_title ) ) {
24686 $plan_title = $this->get_plan_title();
24687 }
24688
24689 if ( $this->is_premium_version_installed() ) {
24690 /**
24691 * If the premium version is already installed, instead of showing the installation instructions,
24692 * tell the current user to activate it.
24693 *
24694 * @author Leo Fajardo (@leorw)
24695 * @since 2.2.1
24696 */
24697 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24698 $this->get_premium_slug() :
24699 $this->premium_plugin_basename();
24700
24701 if ( is_admin() ) {
24702 return sprintf(
24703 /* translators: %1$s: Product title; %2$s: Plan title */
24704 $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting from the %2$s features. %3$s', 'activate-premium-version' ),
24705 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24706 $plan_title,
24707 sprintf(
24708 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24709 ( $this->is_theme() ?
24710 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24711 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24712 esc_html( sprintf(
24713 /* translators: %s: Plan title */
24714 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24715 $plan_title
24716 ) )
24717 )
24718 );
24719 } else {
24720 return sprintf(
24721 /* translators: %1$s: Product title; %3$s: Plan title */
24722 $this->get_text_inline( ' The paid version of %1$s is already installed. Please navigate to the %2$s to activate it and start benefiting from the %3$s features.', 'activate-premium-version-plugins-page' ),
24723 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24724 sprintf(
24725 '<a href="%s">%s</a>',
24726 admin_url( $this->is_theme() ? 'themes.php' : 'plugins.php' ),
24727 ( $this->is_theme() ?
24728 $this->get_text_inline( 'Themes page', 'themes-page' ) :
24729 $this->get_text_inline( 'Plugins page', 'plugins-page' ) )
24730 ),
24731 $plan_title
24732 );
24733 }
24734 } else {
24735 // @since 1.2.1.5 The free version is auto deactivated.
24736 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24737 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24738 '';
24739
24740 return sprintf(
24741 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24742 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24743 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24744 $this->get_latest_download_link( sprintf(
24745 /* translators: %s: Plan title */
24746 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24747 $plan_title
24748 ) ),
24749 $deactivation_step,
24750 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24751 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24752 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24753 );
24754 }
24755 }
24756
24757 /**
24758 * @author Leo Fajardo (@leorw)
24759 * @since 2.5.3
24760 *
24761 * @param string $message_before_the_instructions
24762 * @param string $message_id
24763 * @param string $plan_title
24764 */
24765 private function add_complete_upgrade_instructions_notice(
24766 $message_before_the_instructions,
24767 $message_id,
24768 $plan_title = ''
24769 ) {
24770 $this->_admin_notices->add_sticky(
24771 $message_before_the_instructions .
24772 $this->get_complete_upgrade_instructions( $plan_title ),
24773 $message_id,
24774 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
24775 );
24776 }
24777
24778 /**
24779 * @author Leo Fajardo (@leorw)
24780 * @since 2.5.3
24781 *
24782 * @param bool $is_upgrade
24783 */
24784 private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) {
24785 $this->add_complete_upgrade_instructions_notice(
24786 $is_upgrade ?
24787 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) :
24788 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ),
24789 'plan_upgraded'
24790 );
24791 }
24792
24793 /**
24794 * @author Leo Fajardo (@leorw)
24795 * @since 2.1.0
24796 *
24797 * @param string $url
24798 * @param array $request
24799 * @param int $success_cache_expiration
24800 * @param int $failure_cache_expiration
24801 * @param bool $maybe_enrich_request_for_debug
24802 *
24803 * @return WP_Error|array
24804 */
24805 static function safe_remote_post(
24806 &$url,
24807 $request,
24808 $success_cache_expiration = 0,
24809 $failure_cache_expiration = 0,
24810 $maybe_enrich_request_for_debug = true
24811 ) {
24812 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
24813
24814 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
24815
24816 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
24817 get_transient( $cache_key ) :
24818 false;
24819
24820 if ( false === $response ) {
24821 if ( $maybe_enrich_request_for_debug ) {
24822 FS_DebugManager::enrich_request_for_debug( $url, $request );
24823 }
24824
24825 if ( ! isset( $request['method'] ) ) {
24826 $request['method'] = 'POST';
24827 }
24828
24829 $response = FS_Api::remote_request( $url, $request );
24830
24831 if (
24832 'https://' === substr( $url, 0, 8 ) &&
24833 FS_Api::is_ssl_error_response( $response )
24834 ) {
24835 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
24836 $url = 'http://' . substr( $url, 8 );
24837
24838 $request['timeout'] = 15;
24839 $response = FS_Api::remote_request( $url, $request );
24840 }
24841
24842 if ( false !== $cache_key ) {
24843 set_transient(
24844 $cache_key,
24845 $response,
24846 ( ( $response instanceof WP_Error ) ?
24847 $failure_cache_expiration :
24848 $success_cache_expiration )
24849 );
24850 }
24851 }
24852
24853 return $response;
24854 }
24855
24856 /**
24857 * This method is used to enrich the after upgrade notice instructions when the upgraded
24858 * license cannot be activated network wide (license quota isn't large enough).
24859 *
24860 * @author Vova Feldman (@svovaf)
24861 * @since 2.0.0
24862 *
24863 * @return string
24864 */
24865 private function get_license_network_activation_notice() {
24866 if ( ! $this->_is_network_active ) {
24867 // Module isn't network level activated.
24868 return '';
24869 }
24870
24871 if ( ! fs_is_network_admin() ) {
24872 // Not network level admin.
24873 return '';
24874 }
24875
24876 if ( get_blog_count() == 1 ) {
24877 // There's only a single site in the network so if there's a context license it was already activated.
24878 return '';
24879 }
24880
24881 if ( ! is_object( $this->_license ) ) {
24882 // No context license.
24883 return '';
24884 }
24885
24886 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
24887 // 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).
24888 return '';
24889 }
24890
24891 if ( $this->can_activate_license_on_network( $this->_license ) ) {
24892 // 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).
24893 return '';
24894 }
24895
24896 return sprintf(
24897 $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' ),
24898 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
24899 '</a>'
24900 );
24901 }
24902
24903 /**
24904 * @author Vova Feldman (@svovaf)
24905 * @since 1.2.1.7
24906 *
24907 * @param string $key
24908 *
24909 * @return string
24910 */
24911 function get_text( $key ) {
24912 return fs_text( $key, $this->_slug );
24913 }
24914
24915 /**
24916 * @author Vova Feldman (@svovaf)
24917 * @since 1.2.3
24918 *
24919 * @param string $text Translatable string.
24920 * @param string $key String key for overrides.
24921 *
24922 * @return string
24923 */
24924 function get_text_inline( $text, $key = '' ) {
24925 return _fs_text_inline( $text, $key, $this->_slug );
24926 }
24927
24928 /**
24929 * @author Vova Feldman (@svovaf)
24930 * @since 1.2.3
24931 *
24932 * @param string $text Translatable string.
24933 * @param string $context Context information for the translators.
24934 * @param string $key String key for overrides.
24935 *
24936 * @return string
24937 */
24938 function get_text_x_inline( $text, $context, $key ) {
24939 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
24940 }
24941
24942 /**
24943 * @author Vova Feldman (@svovaf)
24944 * @since 1.2.3
24945 *
24946 * @param string $text Translatable string.
24947 * @param string $key String key for overrides.
24948 *
24949 * @return string
24950 */
24951 function esc_html_inline( $text, $key ) {
24952 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
24953 }
24954
24955 #----------------------------------------------------------------------------------
24956 #region Versioning
24957 #----------------------------------------------------------------------------------
24958
24959 /**
24960 * Check if Freemius in SDK upgrade mode.
24961 *
24962 * @author Vova Feldman (@svovaf)
24963 * @since 1.0.9
24964 *
24965 * @return bool
24966 */
24967 function is_sdk_upgrade_mode() {
24968 return isset( $this->_storage->sdk_upgrade_mode ) ?
24969 $this->_storage->sdk_upgrade_mode :
24970 false;
24971 }
24972
24973 /**
24974 * Turn SDK upgrade mode off.
24975 *
24976 * @author Vova Feldman (@svovaf)
24977 * @since 1.0.9
24978 */
24979 function set_sdk_upgrade_complete() {
24980 $this->_storage->sdk_upgrade_mode = false;
24981 }
24982
24983 /**
24984 * Check if plugin upgrade mode.
24985 *
24986 * @author Vova Feldman (@svovaf)
24987 * @since 1.0.9
24988 *
24989 * @return bool
24990 */
24991 function is_plugin_upgrade_mode() {
24992 return isset( $this->_storage->plugin_upgrade_mode ) ?
24993 $this->_storage->plugin_upgrade_mode :
24994 false;
24995 }
24996
24997 /**
24998 * Turn plugin upgrade mode off.
24999 *
25000 * @author Vova Feldman (@svovaf)
25001 * @since 1.0.9
25002 */
25003 function set_plugin_upgrade_complete() {
25004 $this->_storage->plugin_upgrade_mode = false;
25005
25006 $license_migration = ! empty( $this->_storage->license_migration ) ?
25007 $this->_storage->license_migration :
25008 array();
25009
25010 $license_migration['is_migrating'] = false;
25011
25012 $this->_storage->license_migration = $license_migration;
25013 }
25014
25015 #endregion
25016
25017 #----------------------------------------------------------------------------------
25018 #region Permissions
25019 #----------------------------------------------------------------------------------
25020
25021 /**
25022 * Check if specific permission requested.
25023 *
25024 * @author Vova Feldman (@svovaf)
25025 * @since 1.1.6
25026 *
25027 * @param string $permission
25028 *
25029 * @return bool
25030 */
25031 function is_permission_requested( $permission ) {
25032 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25033 }
25034
25035 #endregion
25036
25037 #----------------------------------------------------------------------------------
25038 #region Auto Activation
25039 #----------------------------------------------------------------------------------
25040
25041 /**
25042 * Hints the SDK if running an auto-installation.
25043 *
25044 * @var bool
25045 */
25046 private $_isAutoInstall = false;
25047
25048 /**
25049 * After upgrade callback to install and auto activate a plugin.
25050 * This code will only be executed on explicit request from the user,
25051 * following the practice Jetpack are using with their theme installations.
25052 *
25053 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25054 *
25055 * @author Vova Feldman (@svovaf)
25056 * @since 1.2.1.7
25057 */
25058 function _install_premium_version_ajax_action() {
25059 $this->_logger->entrance();
25060
25061 $this->check_ajax_referer( 'install_premium_version' );
25062
25063 if ( ! $this->is_registered() ) {
25064 // Not registered.
25065 self::shoot_ajax_failure( array(
25066 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25067 'code' => 'premium_installed',
25068 ) );
25069 }
25070
25071 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25072
25073 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25074 // Invalid ID.
25075 self::shoot_ajax_failure( array(
25076 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25077 'code' => 'invalid_module_id',
25078 ) );
25079 }
25080
25081 if ( $plugin_id == $this->get_id() ) {
25082 if ( $this->is_premium() ) {
25083 // Already using the premium code version.
25084 self::shoot_ajax_failure( array(
25085 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25086 'code' => 'premium_installed',
25087 ) );
25088 }
25089 if ( ! $this->can_use_premium_code() ) {
25090 // Don't have access to the premium code.
25091 self::shoot_ajax_failure( array(
25092 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25093 'code' => 'invalid_license',
25094 ) );
25095 }
25096 if ( ! $this->has_release_on_freemius() ) {
25097 // Plugin is a serviceware, no premium code version.
25098 self::shoot_ajax_failure( array(
25099 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25100 'code' => 'premium_version_missing',
25101 ) );
25102 }
25103 } else {
25104 $addon = $this->get_addon( $plugin_id );
25105
25106 if ( ! is_object( $addon ) ) {
25107 // Invalid add-on ID.
25108 self::shoot_ajax_failure( array(
25109 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25110 'code' => 'invalid_module_id',
25111 ) );
25112 }
25113
25114 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25115 // Premium add-on version is already activated.
25116 self::shoot_ajax_failure( array(
25117 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25118 'code' => 'premium_installed',
25119 ) );
25120 }
25121 }
25122
25123 $this->_isAutoInstall = true;
25124
25125 // Try to install and activate.
25126 $updater = FS_Plugin_Updater::instance( $this );
25127 $result = $updater->install_and_activate_plugin( $plugin_id );
25128
25129 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25130 self::shoot_ajax_failure( array(
25131 'message' => $result['message'],
25132 'code' => $result['code'],
25133 ) );
25134 }
25135
25136 self::shoot_ajax_success( $result );
25137 }
25138
25139 /**
25140 * Displays module activation dialog box after a successful upgrade
25141 * where the user explicitly requested to auto download and install
25142 * the premium version.
25143 *
25144 * @author Vova Feldman (@svovaf)
25145 * @since 1.2.1.7
25146 */
25147 function _add_auto_installation_dialog_box() {
25148 $this->_logger->entrance();
25149
25150 if ( ! $this->is_registered() ) {
25151 // Not registered.
25152 return;
25153 }
25154
25155 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25156
25157 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25158 // Invalid module ID.
25159 return;
25160 }
25161
25162 if ( $plugin_id == $this->get_id() ) {
25163 if ( $this->is_premium() ) {
25164 // Already using the premium code version.
25165 return;
25166 }
25167 if ( ! $this->can_use_premium_code() ) {
25168 // Don't have access to the premium code.
25169 return;
25170 }
25171 if ( ! $this->has_release_on_freemius() ) {
25172 // Plugin is a serviceware, no premium code version.
25173 return;
25174 }
25175 } else {
25176 $addon = $this->get_addon( $plugin_id );
25177
25178 if ( ! is_object( $addon ) ) {
25179 // Invalid add-on ID.
25180 return;
25181 }
25182
25183 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25184 // Premium add-on version is already activated.
25185 return;
25186 }
25187 }
25188
25189 $vars = array(
25190 'id' => $this->_module_id,
25191 'target_module_id' => $plugin_id,
25192 'slug' => $this->_slug,
25193 );
25194
25195 fs_require_template( 'auto-installation.php', $vars );
25196 }
25197
25198 #endregion
25199
25200 #--------------------------------------------------------------------------------
25201 #region Tabs Integration
25202 #--------------------------------------------------------------------------------
25203
25204 #region Module's Original Tabs
25205
25206 /**
25207 * Inject a JavaScript logic to capture the theme tabs HTML.
25208 *
25209 * @author Vova Feldman (@svovaf)
25210 * @since 1.2.2.7
25211 */
25212 function _tabs_capture() {
25213 $this->_logger->entrance();
25214
25215 if (
25216 ! $this->is_product_settings_page() ||
25217 ! $this->should_page_include_tabs() ||
25218 ! $this->is_matching_url( $this->main_menu_url() )
25219 ) {
25220 return;
25221 }
25222
25223 $params = array(
25224 'id' => $this->_module_id,
25225 );
25226
25227 fs_require_once_template( 'tabs-capture-js.php', $params );
25228 }
25229
25230 /**
25231 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25232 * after version and type (free/premium) changes, in addition to the week period.
25233 *
25234 * @author Vova Feldman (@svovaf)
25235 * @since 1.2.2.7
25236 */
25237 function _store_tabs_ajax_action() {
25238 $this->_logger->entrance();
25239
25240 $this->check_ajax_referer( 'store_tabs' );
25241
25242 // Init filesystem if not yet initiated.
25243 WP_Filesystem();
25244
25245 // Get POST body HTML data.
25246 global $wp_filesystem;
25247 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25248
25249 if ( is_string( $tabs_html ) ) {
25250 $tabs_html = trim( $tabs_html );
25251 }
25252
25253 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25254 self::shoot_ajax_failure();
25255 }
25256
25257 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25258
25259 self::shoot_ajax_success();
25260 }
25261
25262 /**
25263 * Cache theme's settings page custom styles. The cache will also be set as expired
25264 * after version and type (free/premium) changes, in addition to the week period.
25265 *
25266 * @author Vova Feldman (@svovaf)
25267 * @since 1.2.2.7
25268 */
25269 function _store_tabs_styles() {
25270 $this->_logger->entrance();
25271
25272 if (
25273 ! $this->is_product_settings_page() ||
25274 ! $this->should_page_include_tabs() ||
25275 ! $this->is_matching_url( $this->main_menu_url() )
25276 ) {
25277 return;
25278 }
25279
25280 $wp_styles = wp_styles();
25281
25282 $theme_styles_url = get_template_directory_uri();
25283
25284 $stylesheets = array();
25285 foreach ( $wp_styles->queue as $handler ) {
25286 if ( fs_starts_with( $handler, 'fs_' ) ) {
25287 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25288 continue;
25289 }
25290
25291 /**
25292 * @var _WP_Dependency $stylesheet
25293 */
25294 $stylesheet = $wp_styles->registered[ $handler ];
25295
25296 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25297 $stylesheets[] = $stylesheet->src;
25298 }
25299 }
25300
25301 if ( ! empty( $stylesheets ) ) {
25302 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25303 }
25304 }
25305
25306 /**
25307 * Check if module's original settings page has any tabs.
25308 *
25309 * @author Vova Feldman (@svovaf)
25310 * @since 1.2.2.7
25311 *
25312 * @return bool
25313 */
25314 private function has_tabs() {
25315 return $this->_cache->has( 'tabs' );
25316 }
25317
25318 /**
25319 * Get module's settings page HTML content, starting
25320 * from the beginning of the <div class="wrap"> element,
25321 * until the tabs HTML (including).
25322 *
25323 * @author Vova Feldman (@svovaf)
25324 * @since 1.2.2.7
25325 *
25326 * @return string
25327 */
25328 private function get_tabs_html() {
25329 $this->_logger->entrance();
25330
25331 return $this->_cache->get( 'tabs' );
25332 }
25333
25334 /**
25335 * Check if page should include tabs.
25336 *
25337 * @author Vova Feldman (@svovaf)
25338 * @since 1.2.2.7
25339 *
25340 * @return bool
25341 */
25342 private function should_page_include_tabs() {
25343 if ( ! $this->has_settings_menu() ) {
25344 // Don't add tabs if no settings at all.
25345 return false;
25346 }
25347
25348 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25349 // Only add tabs to themes for now.
25350 return false;
25351 }
25352
25353 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25354 // Only add tabs to monetizing themes.
25355 return false;
25356 }
25357
25358 if ( ! $this->is_product_settings_page() ) {
25359 // Only add tabs if browsing one of the product's setting pages.
25360 return false;
25361 }
25362
25363 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25364 // Don't include tabs in the activation page.
25365 return false;
25366 }
25367
25368 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25369 // Don't add tabs on checkout page, we want to reduce distractions
25370 // as much as possible.
25371 return false;
25372 }
25373
25374 return true;
25375 }
25376
25377 /**
25378 * Add the tabs HTML before the setting's page content and
25379 * enqueue any required stylesheets.
25380 *
25381 * @author Vova Feldman (@svovaf)
25382 * @since 1.2.2.7
25383 *
25384 * @return bool If tabs were included.
25385 */
25386 function _add_tabs_before_content() {
25387 $this->_logger->entrance();
25388
25389 if ( ! $this->should_page_include_tabs() ) {
25390 return false;
25391 }
25392
25393 $tabs_html = $this->get_tabs_html();
25394
25395 if ( empty( $tabs_html ) ) {
25396 return false;
25397 }
25398
25399 /**
25400 * Enqueue the original stylesheets that are included in the
25401 * theme settings page. That way, if the theme settings has
25402 * some custom _styled_ content above the tabs UI, this
25403 * will make sure that the styling is preserved.
25404 */
25405 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25406 if ( is_array( $stylesheets ) ) {
25407 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25408 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25409 }
25410 }
25411
25412 // Cut closing </div> tag.
25413 echo substr( trim( $tabs_html ), 0, - 6 );
25414
25415 return true;
25416 }
25417
25418 /**
25419 * Add the tabs closing HTML after the setting's page content.
25420 *
25421 * @author Vova Feldman (@svovaf)
25422 * @since 1.2.2.7
25423 *
25424 * @return bool If tabs closing HTML was included.
25425 */
25426 function _add_tabs_after_content() {
25427 $this->_logger->entrance();
25428
25429 if ( ! $this->should_page_include_tabs() ) {
25430 return false;
25431 }
25432
25433 echo '</div>';
25434
25435 return true;
25436 }
25437
25438 #endregion
25439
25440 /**
25441 * Add in-page JavaScript to inject the Freemius tabs into
25442 * the module's setting tabs section.
25443 *
25444 * @author Vova Feldman (@svovaf)
25445 * @since 1.2.2.7
25446 */
25447 function _add_freemius_tabs() {
25448 $this->_logger->entrance();
25449
25450 if ( ! $this->should_page_include_tabs() ) {
25451 return;
25452 }
25453
25454 $params = array( 'id' => $this->_module_id );
25455 fs_require_once_template( 'tabs.php', $params );
25456 }
25457
25458 #endregion
25459
25460 #--------------------------------------------------------------------------------
25461 #region Customizer Integration for Themes
25462 #--------------------------------------------------------------------------------
25463
25464 /**
25465 * @author Vova Feldman (@svovaf)
25466 * @since 1.2.2.7
25467 *
25468 * @param WP_Customize_Manager $customizer
25469 */
25470 function _customizer_register( $customizer ) {
25471 $this->_logger->entrance();
25472
25473 if ( $this->is_pricing_page_visible() ) {
25474 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25475
25476 $customizer->add_section( 'freemius_upsell', array(
25477 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25478 'priority' => 1,
25479 ) );
25480 $customizer->add_setting( 'freemius_upsell', array(
25481 'sanitize_callback' => 'esc_html',
25482 ) );
25483
25484 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25485 'fs' => $this,
25486 'section' => 'freemius_upsell',
25487 'priority' => 100,
25488 ) ) );
25489 }
25490
25491 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25492 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25493
25494 // Main Documentation Link In Customizer Root.
25495 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25496 'fs' => $this,
25497 'priority' => 1000,
25498 ) ) );
25499 }
25500 }
25501
25502 #endregion
25503
25504 /**
25505 * If the theme has a paid version, add some custom
25506 * styling to the theme's premium version (if exists)
25507 * to highlight that it's the premium version of the
25508 * same theme, making it easier for identification
25509 * after the user upgrades and upload it to the site.
25510 *
25511 * @author Vova Feldman (@svovaf)
25512 * @since 1.2.2.7
25513 */
25514 function _style_premium_theme() {
25515 $this->_logger->entrance();
25516
25517 if ( ! self::is_themes_page() ) {
25518 // Only include in the themes page.
25519 return;
25520 }
25521
25522 if ( ! $this->has_paid_plan() ) {
25523 // Only include if has any paid plans.
25524 return;
25525 }
25526
25527 $params = null;
25528 fs_require_once_template( '/js/jquery.content-change.php', $params );
25529
25530 $params = array(
25531 'slug' => $this->_slug,
25532 'id' => $this->_module_id,
25533 );
25534
25535 fs_require_template( '/js/style-premium-theme.php', $params );
25536 }
25537
25538 /**
25539 * This method will return the absolute URL of the module's local icon.
25540 *
25541 * When you are running your plugin or theme on a **localhost** environment, if the icon
25542 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25543 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25544 * If an icon is found, this method will automatically attempt to download the icon and store it
25545 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25546 *
25547 * It's important to mention that this method is NOT phoning home since the developer will deploy
25548 * the product with the local icon in the assets folder. The download process just simplifies
25549 * the process for the developer.
25550 *
25551 * @author Vova Feldman (@svovaf)
25552 * @since 2.0.0
25553 *
25554 * @return string
25555 */
25556 function get_local_icon_url() {
25557 global $fs_active_plugins;
25558
25559 /**
25560 * @since 1.1.7.5
25561 */
25562 $local_path = $this->apply_filters( 'plugin_icon', false );
25563
25564 if ( is_string( $local_path ) ) {
25565 $icons = array( $local_path );
25566 } else {
25567 $img_dir = WP_FS__DIR_IMG;
25568
25569 // Locate the main assets folder.
25570 if ( ! empty( $fs_active_plugins->plugins ) ) {
25571 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25572
25573 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25574 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25575 $img_dir = $plugin_or_theme_img_dir
25576 . '/'
25577 /**
25578 * The basename will be `themes` or the basename of a custom themes directory.
25579 *
25580 * @author Leo Fajardo (@leorw)
25581 * @since 2.2.3
25582 */
25583 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25584 . '/assets/img';
25585
25586 break;
25587 }
25588 }
25589 }
25590
25591 // Try to locate the icon in the assets folder.
25592 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25593
25594 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25595 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25596 $icons = array(
25597 fs_normalize_path( $img_dir . '/theme-icon.png' )
25598 );
25599 } else {
25600 $icon_found = false;
25601 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25602
25603 if ( ! function_exists( 'get_filesystem_method' ) ) {
25604 require_once ABSPATH . 'wp-admin/includes/file.php';
25605 }
25606
25607 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25608
25609 /**
25610 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25611 *
25612 * This code will only be executed once during the testing
25613 * of the plugin in a local environment. The plugin icon file WILL
25614 * already exist in the assets folder when the plugin is deployed to
25615 * the repository.
25616 */
25617 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25618 // Fetch icon from Freemius.
25619 $icon = $this->fetch_remote_icon_url();
25620
25621 // Fetch icon from WordPress.org.
25622 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25623 if ( ! function_exists( 'plugins_api' ) ) {
25624 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25625 }
25626
25627 $plugin_information = plugins_api( 'plugin_information', array(
25628 'slug' => $this->_slug,
25629 'fields' => array(
25630 'sections' => false,
25631 'tags' => false,
25632 'icons' => true
25633 )
25634 ) );
25635
25636 if (
25637 ! is_wp_error( $plugin_information )
25638 && isset( $plugin_information->icons )
25639 && ! empty( $plugin_information->icons )
25640 ) {
25641 /**
25642 * Get the smallest icon.
25643 *
25644 * @author Leo Fajardo (@leorw)
25645 * @since 1.2.2
25646 */
25647 $icon = end( $plugin_information->icons );
25648 }
25649 }
25650
25651 if ( ! empty( $icon ) ) {
25652 if ( 0 !== strpos( $icon, 'http' ) ) {
25653 $icon = 'http:' . $icon;
25654 }
25655
25656 /**
25657 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25658 *
25659 * @author Leo Fajardo (@leorw)
25660 * @since 1.2.2
25661 */
25662 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25663
25664 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25665
25666 // Try to download the icon.
25667 $icon_found = fs_download_image( $icon, $local_path );
25668 }
25669 }
25670
25671 if ( ! $icon_found ) {
25672 // No icons found, fallback to default icon.
25673 if ( $have_write_permissions ) {
25674 // If have write permissions, copy default icon.
25675 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25676 } else {
25677 // If doesn't have write permissions, use default icon path.
25678 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25679 }
25680 }
25681
25682 $icons = array( $local_path );
25683 }
25684 }
25685 }
25686
25687 $icon_dir = dirname( $icons[0] );
25688
25689 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25690 }
25691
25692 /**
25693 * Fetch module's extended info.
25694 *
25695 * @author Vova Feldman (@svovaf)
25696 * @since 2.0.0
25697 *
25698 * @return object|mixed
25699 */
25700 private function fetch_module_info() {
25701 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25702 }
25703
25704 /**
25705 * Fetch module's remote icon URL.
25706 *
25707 * @author Vova Feldman (@svovaf)
25708 * @since 2.0.0
25709 *
25710 * @return string
25711 */
25712 function fetch_remote_icon_url() {
25713 $info = $this->fetch_module_info();
25714
25715 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25716 $info->icon :
25717 '';
25718 }
25719
25720 #--------------------------------------------------------------------------------
25721 #region GDPR
25722 #--------------------------------------------------------------------------------
25723
25724 /**
25725 * @author Leo Fajardo (@leorw)
25726 * @since 2.1.0
25727 *
25728 * @param array $user_plugins
25729 *
25730 * @return string
25731 */
25732 private function get_gdpr_admin_notice_string( $user_plugins ) {
25733 $this->_logger->entrance();
25734
25735 $addons = self::get_all_addons();
25736
25737 foreach ( $user_plugins as $user_plugin ) {
25738 $has_addons = isset( $addons[ $user_plugin->id ] );
25739
25740 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25741 if ( $this->_module_id == $user_plugin->id ) {
25742 $addons = $this->get_addons();
25743 $has_addons = ( ! empty( $addons ) );
25744 } else {
25745 $plugin_api = FS_Api::instance(
25746 $user_plugin->id,
25747 'plugin',
25748 $user_plugin->id,
25749 $user_plugin->public_key,
25750 ! $user_plugin->is_live,
25751 false,
25752 $this->get_sdk_version()
25753 );
25754
25755 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25756
25757 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25758 is_array( $addons_result->plugins ) &&
25759 ! empty( $addons_result->plugins )
25760 ) {
25761 $has_addons = true;
25762 }
25763 }
25764 }
25765
25766 $user_plugin->has_addons = $has_addons;
25767 }
25768
25769 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25770
25771 $multiple_products_text = '';
25772
25773 if ( $is_single_parent_product ) {
25774 $single_parent_product = reset( $user_plugins );
25775
25776 $thank_you = sprintf(
25777 "<span data-plugin-id='%d'>%s</span>",
25778 $single_parent_product->id,
25779 sprintf(
25780 $single_parent_product->has_addons ?
25781 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25782 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25783 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25784 )
25785 );
25786
25787 $already_opted_in = sprintf(
25788 $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' ),
25789 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
25790 );
25791 } else {
25792 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
25793 $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' );
25794
25795 $products_and_add_ons = '';
25796 foreach ( $user_plugins as $user_plugin ) {
25797 if ( ! empty( $products_and_add_ons ) ) {
25798 $products_and_add_ons .= ', ';
25799 }
25800
25801 if ( ! $user_plugin->has_addons ) {
25802 $products_and_add_ons .= sprintf(
25803 "<span data-plugin-id='%d'>%s</span>",
25804 $user_plugin->id,
25805 $user_plugin->title
25806 );
25807 } else {
25808 $products_and_add_ons .= sprintf(
25809 "<span data-plugin-id='%d'>%s</span>",
25810 $user_plugin->id,
25811 sprintf(
25812 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
25813 $user_plugin->title
25814 )
25815 );
25816 }
25817 }
25818
25819 $multiple_products_text = sprintf(
25820 "<small class='products'><strong>%s:</strong> %s</small>",
25821 $this->get_text_inline( 'Products', 'products' ),
25822 $products_and_add_ons
25823 );
25824 }
25825
25826 $actions = sprintf(
25827 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
25828 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
25829 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
25830 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
25831 sprintf(
25832 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
25833 '<span class="underlined">',
25834 '</span>'
25835 )
25836 );
25837
25838 return sprintf(
25839 '%s %s %s',
25840 $thank_you,
25841 $already_opted_in,
25842 sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '<a href="https://ec.europa.eu/info/law/law-topic/data-protection_en/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
25843 '<br><br>' .
25844 '<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>' .
25845 $actions .
25846 ( $is_single_parent_product ? '' : $multiple_products_text )
25847 );
25848 }
25849
25850 /**
25851 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
25852 * plugins and themes they've opted in to.
25853 *
25854 * @author Leo Fajardo (@leorw)
25855 * @since 2.1.0
25856 *
25857 * @param string $user_email
25858 * @param string $license_key
25859 * @param array $plugin_ids
25860 * @param string|null $license_key
25861 *
25862 * @return array|false
25863 */
25864 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
25865 $request = array(
25866 'method' => 'POST',
25867 'body' => array(),
25868 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
25869 );
25870
25871 if ( is_string( $user_email ) ) {
25872 $request['body']['email'] = $user_email;
25873 } else {
25874 $request['body']['license_key'] = $license_key;
25875 }
25876
25877 $result = array();
25878
25879 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
25880 $total_plugin_ids = count( $plugin_ids );
25881
25882 $plugin_ids_count_per_request = 10;
25883 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
25884 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
25885
25886 $request['body']['plugin_ids'] = $plugin_ids_set;
25887
25888 $response = self::safe_remote_post(
25889 $url,
25890 $request,
25891 WP_FS__TIME_24_HOURS_IN_SEC,
25892 WP_FS__TIME_12_HOURS_IN_SEC
25893 );
25894
25895 if ( ! is_wp_error( $response ) ) {
25896 $decoded = is_string( $response['body'] ) ?
25897 json_decode( $response['body'] ) :
25898 null;
25899
25900 if (
25901 !is_object($decoded) ||
25902 !isset($decoded->success) ||
25903 true !== $decoded->success ||
25904 !isset( $decoded->data ) ||
25905 !is_array( $decoded->data )
25906 ) {
25907 return false;
25908 }
25909
25910 $result = array_merge( $result, $decoded->data );
25911 }
25912 }
25913
25914 return $result;
25915 }
25916
25917 /**
25918 * @author Leo Fajardo (@leorw)
25919 * @since 2.1.0
25920 */
25921 function _maybe_show_gdpr_admin_notice() {
25922 if ( ! $this->is_user_in_admin() ) {
25923 return;
25924 }
25925
25926 if ( ! $this->should_handle_gdpr_admin_notice() ) {
25927 return;
25928 }
25929
25930 if ( ! $this->is_user_admin() ) {
25931 return;
25932 }
25933
25934 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
25935
25936 $lock = FS_User_Lock::instance();
25937
25938 /**
25939 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
25940 */
25941 if ( ! $lock->try_lock( 60 ) ) {
25942 return;
25943 }
25944
25945 /**
25946 * @var $current_wp_user WP_User
25947 */
25948 $current_wp_user = self::_get_current_wp_user();
25949
25950 /**
25951 * @var FS_User $current_fs_user
25952 */
25953 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
25954
25955 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
25956
25957 if ( ! is_object( $current_fs_user ) ) {
25958 // 10-year lock.
25959 $lock->lock( $ten_years_in_sec );
25960
25961 return;
25962 }
25963
25964 $gdpr = FS_GDPR_Manager::instance();
25965
25966 if ( $gdpr->is_opt_in_notice_shown() ) {
25967 // 30-day lock.
25968 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
25969
25970 return;
25971 }
25972
25973 if ( ! $gdpr->should_show_opt_in_notice() ) {
25974 // 10-year lock.
25975 $lock->lock( $ten_years_in_sec );
25976
25977 return;
25978 }
25979
25980 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
25981 $was_notice_shown_before = ( false !== $last_time_notice_shown );
25982
25983 if ( $was_notice_shown_before &&
25984 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
25985 ) {
25986 // If the notice was shown before, show it again after 30 days from the last time it was shown.
25987 return;
25988 }
25989
25990 /**
25991 * Find all plugin IDs that were installed by the current admin.
25992 */
25993 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
25994
25995 if ( empty( $plugin_ids_map )) {
25996 $lock->lock( $ten_years_in_sec );
25997
25998 return;
25999 }
26000
26001 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26002 $current_fs_user->email,
26003 null,
26004 array_keys( $plugin_ids_map )
26005 );
26006
26007 if ( empty( $user_plugins ) ) {
26008 $lock->lock(
26009 is_array($user_plugins) ?
26010 $ten_years_in_sec :
26011 // Lock for 24-hours on errors.
26012 WP_FS__TIME_24_HOURS_IN_SEC
26013 );
26014
26015 return;
26016 }
26017
26018 $has_unset_marketing_optin = false;
26019
26020 foreach ( $user_plugins as $user_plugin ) {
26021 if ( true == $user_plugin->is_marketing_allowed ) {
26022 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26023 }
26024
26025 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26026 $has_unset_marketing_optin = true;
26027 }
26028 }
26029
26030 if ( empty( $plugin_ids_map ) ||
26031 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26032 ) {
26033 $lock->lock( $ten_years_in_sec );
26034
26035 return;
26036 }
26037
26038 $modules = array_merge(
26039 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26040 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26041 );
26042
26043 foreach ( $modules as $module ) {
26044 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26045 $plugin_ids_map[ $module->id ] = $module;
26046 }
26047 }
26048
26049 $plugin_title = null;
26050 if ( 1 === count( $plugin_ids_map ) ) {
26051 $module = reset( $plugin_ids_map );
26052 $plugin_title = $module->title;
26053 }
26054
26055 $gdpr->add_opt_in_sticky_notice(
26056 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26057 $plugin_title
26058 );
26059
26060 $this->add_gdpr_optin_ajax_handler_and_style();
26061
26062 $gdpr->notice_was_just_shown();
26063
26064 // 30-day lock.
26065 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26066 }
26067
26068 /**
26069 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26070 * marketing.
26071 *
26072 * @author Leo Fajardo (@leorw)
26073 * @since 2.1.0
26074 */
26075 private function disable_opt_in_notice_and_lock_user() {
26076 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26077
26078 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26079
26080 // 10-year lock.
26081 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26082 }
26083
26084 /**
26085 * @author Leo Fajardo (@leorw)
26086 * @since 2.5.4
26087 */
26088 static function _add_api_connectivity_notice_handler_js() {
26089 fs_require_once_template( 'api-connectivity-message-js.php' );
26090 }
26091
26092 /**
26093 * @author Leo Fajardo (@leorw)
26094 * @since 2.1.0
26095 */
26096 function _add_gdpr_optin_js() {
26097 $vars = array( 'id' => $this->_module_id );
26098
26099 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26100 }
26101
26102 /**
26103 * @author Leo Fajardo (@leorw)
26104 * @since 2.1.0
26105 */
26106 function enqueue_gdpr_optin_notice_style() {
26107 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26108 }
26109
26110 /**
26111 * @author Leo Fajardo (@leorw)
26112 * @since 2.1.0
26113 */
26114 function _maybe_add_gdpr_optin_ajax_handler() {
26115 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26116
26117 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26118 $this->add_gdpr_optin_ajax_handler_and_style();
26119 }
26120 }
26121
26122 /**
26123 * @author Leo Fajardo (@leorw)
26124 * @since 2.1.0
26125 */
26126 function _fetch_is_marketing_required_flag_value_ajax_action() {
26127 $this->_logger->entrance();
26128
26129 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26130
26131 $license_key = fs_request_get_raw( 'license_key' );
26132
26133 if ( empty($license_key) ) {
26134 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26135 }
26136
26137 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26138 null,
26139 $license_key,
26140 array( $this->_module_id )
26141 );
26142
26143 if ( ! is_array( $user_plugins ) ||
26144 empty($user_plugins) ||
26145 !isset($user_plugins[0]->plugin_id) ||
26146 $user_plugins[0]->plugin_id != $this->_module_id
26147 ) {
26148 /**
26149 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26150 *
26151 * @author Vova Feldman (@svovaf)
26152 */
26153 self::shoot_ajax_success( array(
26154 'is_marketing_allowed' => null,
26155 'license_owner_id' => null
26156 ) );
26157 }
26158
26159 self::shoot_ajax_success( array(
26160 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26161 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26162 ) );
26163 }
26164
26165 /**
26166 * @author Leo Fajardo (@leorw)
26167 * @since 2.3.2
26168 *
26169 * @param number[] $install_ids
26170 *
26171 * @return array {
26172 * An array of objects containing the installs' licenses owners data.
26173 *
26174 * @property number $id User ID.
26175 * @property string $email User email (can be masked email).
26176 * }
26177 */
26178 private function fetch_installs_licenses_owners_data( $install_ids ) {
26179 $this->_logger->entrance();
26180
26181 $response = $this->get_api_user_scope()->get(
26182 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26183 );
26184
26185 $license_owners = array();
26186
26187 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26188 $license_owners = $response->owners;
26189 }
26190
26191 return $license_owners;
26192 }
26193
26194 /**
26195 * @author Leo Fajardo (@leorw)
26196 * @since 2.1.0
26197 */
26198 private function add_gdpr_optin_ajax_handler_and_style() {
26199 // Add GDPR action AJAX callback.
26200 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26201
26202 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26203 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26204 }
26205
26206 /**
26207 * @author Leo Fajardo (@leorw)
26208 * @since 2.1.0
26209 */
26210 function _gdpr_optin_ajax_action() {
26211 $this->_logger->entrance();
26212
26213 $this->check_ajax_referer( 'gdpr_optin_action' );
26214
26215 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26216 self::shoot_ajax_failure();
26217 }
26218
26219 $current_wp_user = self::_get_current_wp_user();
26220
26221 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26222 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26223 self::shoot_ajax_failure();
26224 }
26225
26226 $modules = array_merge(
26227 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26228 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26229 );
26230
26231 foreach ( $modules as $key => $module ) {
26232 if ( ! in_array( $module->id, $plugin_ids ) ) {
26233 unset( $modules[ $key ] );
26234 }
26235 }
26236
26237 if ( empty( $modules ) ) {
26238 self::shoot_ajax_failure();
26239 }
26240
26241 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26242
26243 foreach ( $modules as $module ) {
26244 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26245 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26246 ) );
26247 }
26248
26249 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26250
26251 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26252
26253 // 10-year lock.
26254 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26255
26256 self::shoot_ajax_success();
26257 }
26258
26259 /**
26260 * 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.
26261 *
26262 * @author Vova Feldman (@svovaf)
26263 * @since 2.1.0
26264 *
26265 * @return bool
26266 */
26267 private function should_handle_gdpr_admin_notice() {
26268 return $this->apply_filters(
26269 'handle_gdpr_admin_notice',
26270 // Default to false.
26271 false
26272 );
26273 }
26274
26275 #endregion
26276
26277 #----------------------------------------------------------------------------------
26278 #region Marketing
26279 #----------------------------------------------------------------------------------
26280
26281 /**
26282 * Check if current user purchased any other plugins before.
26283 *
26284 * @author Vova Feldman (@svovaf)
26285 * @since 1.0.9
26286 *
26287 * @return bool
26288 */
26289 function has_purchased_before() {
26290 // TODO: Implement has_purchased_before() method.
26291 throw new Exception( 'not implemented' );
26292 }
26293
26294 /**
26295 * Check if current user classified as an agency.
26296 *
26297 * @author Vova Feldman (@svovaf)
26298 * @since 1.0.9
26299 *
26300 * @return bool
26301 */
26302 function is_agency() {
26303 // TODO: Implement is_agency() method.
26304 throw new Exception( 'not implemented' );
26305 }
26306
26307 /**
26308 * Check if current user classified as a developer.
26309 *
26310 * @author Vova Feldman (@svovaf)
26311 * @since 1.0.9
26312 *
26313 * @return bool
26314 */
26315 function is_developer() {
26316 // TODO: Implement is_developer() method.
26317 throw new Exception( 'not implemented' );
26318 }
26319
26320 /**
26321 * Check if current user classified as a business.
26322 *
26323 * @author Vova Feldman (@svovaf)
26324 * @since 1.0.9
26325 *
26326 * @return bool
26327 */
26328 function is_business() {
26329 // TODO: Implement is_business() method.
26330 throw new Exception( 'not implemented' );
26331 }
26332
26333 #endregion
26334
26335 #----------------------------------------------------------------------------------
26336 #region Helper
26337 #----------------------------------------------------------------------------------
26338
26339 /**
26340 * If running with a secret key, assume it's the developer and show pending plans as well.
26341 *
26342 * @author Vova Feldman (@svovaf)
26343 * @since 2.1.2
26344 *
26345 * @param string $path
26346 *
26347 * @return string
26348 */
26349 function add_show_pending( $path ) {
26350 if ( ! $this->has_secret_key() ) {
26351 return $path;
26352 }
26353
26354 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26355 }
26356
26357 #endregion
26358 }
26359